// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/draggable_button.h" #include "base/logging.h" #import "base/memory/scoped_nsobject.h" namespace { // Code taken from <http://codereview.chromium.org/180036/diff/3001/3004>. // TODO(viettrungluu): Do we want common, standard code for drag hysteresis? const CGFloat kWebDragStartHysteresisX = 5.0; const CGFloat kWebDragStartHysteresisY = 5.0; const CGFloat kDragExpirationTimeout = 1.0; } @implementation DraggableButton @synthesize draggable = draggable_; @synthesize actsOnMouseDown = actsOnMouseDown_; @synthesize durationMouseWasDown = durationMouseWasDown_; @synthesize actionHasFired = actionHasFired_; @synthesize whenMouseDown = whenMouseDown_; - (id)initWithFrame:(NSRect)frame { if ((self = [super initWithFrame:frame])) { draggable_ = YES; actsOnMouseDown_ = NO; actionHasFired_ = NO; } return self; } - (id)initWithCoder:(NSCoder*)coder { if ((self = [super initWithCoder:coder])) { draggable_ = YES; actsOnMouseDown_ = NO; actionHasFired_ = NO; } return self; } - (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta yDelta:(float)yDelta xHysteresis:(float)xHysteresis yHysteresis:(float)yHysteresis { return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis); } - (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta yDelta:(float)yDelta xHysteresis:(float)xHysteresis yHysteresis:(float)yHysteresis { return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis); } // Determine whether a mouse down should turn into a drag; started as copy of // NSTableView code. - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent withExpiration:(NSDate*)expiration xHysteresis:(float)xHysteresis yHysteresis:(float)yHysteresis { if ([mouseDownEvent type] != NSLeftMouseDown) { return NO; } NSEvent* nextEvent = nil; NSEvent* firstEvent = nil; NSEvent* dragEvent = nil; NSEvent* mouseUp = nil; BOOL dragIt = NO; while ((nextEvent = [[self window] nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask) untilDate:expiration inMode:NSEventTrackingRunLoopMode dequeue:YES]) != nil) { if (firstEvent == nil) { firstEvent = nextEvent; } if ([nextEvent type] == NSLeftMouseDragged) { float deltax = [nextEvent locationInWindow].x - [mouseDownEvent locationInWindow].x; float deltay = [nextEvent locationInWindow].y - [mouseDownEvent locationInWindow].y; dragEvent = nextEvent; if ([self deltaIndicatesConclusionReachedWithXDelta:deltax yDelta:deltay xHysteresis:xHysteresis yHysteresis:yHysteresis]) { dragIt = [self deltaIndicatesDragStartWithXDelta:deltax yDelta:deltay xHysteresis:xHysteresis yHysteresis:yHysteresis]; break; } } else if ([nextEvent type] == NSLeftMouseUp) { mouseUp = nextEvent; break; } } // Since we've been dequeuing the events (If we don't, we'll never see // the mouse up...), we need to push some of the events back on. // It makes sense to put the first and last drag events and the mouse // up if there was one. if (mouseUp != nil) { [NSApp postEvent:mouseUp atStart:YES]; } if (dragEvent != nil) { [NSApp postEvent:dragEvent atStart:YES]; } if (firstEvent != mouseUp && firstEvent != dragEvent) { [NSApp postEvent:firstEvent atStart:YES]; } return dragIt; } - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent withExpiration:(NSDate*)expiration { return [self dragShouldBeginFromMouseDown:mouseDownEvent withExpiration:expiration xHysteresis:kWebDragStartHysteresisX yHysteresis:kWebDragStartHysteresisY]; } - (void)mouseUp:(NSEvent*)theEvent { durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_; if (actionHasFired_) return; if (!draggable_) { [super mouseUp:theEvent]; return; } // There are non-drag cases where a mouseUp: may happen // (e.g. mouse-down, cmd-tab to another application, move mouse, // mouse-up). So we check. NSPoint viewLocal = [self convertPoint:[theEvent locationInWindow] fromView:[[self window] contentView]]; if (NSPointInRect(viewLocal, [self bounds])) { [self performClick:self]; } } - (void)secondaryMouseUpAction:(BOOL)wasInside { // Override if you want to do any extra work on mouseUp, after a mouseDown // action has already fired. } - (void)performMouseDownAction:(NSEvent*)theEvent { int eventMask = NSLeftMouseUpMask; [[self target] performSelector:[self action] withObject:self]; actionHasFired_ = YES; while (1) { theEvent = [[self window] nextEventMatchingMask:eventMask]; if (!theEvent) continue; NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]]; [self highlight:isInside]; switch ([theEvent type]) { case NSLeftMouseUp: durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_; [self secondaryMouseUpAction:isInside]; break; default: /* Ignore any other kind of event. */ break; } } [self highlight:NO]; } // Mimic "begin a click" operation visually. Do NOT follow through // with normal button event handling. - (void)mouseDown:(NSEvent*)theEvent { [[NSCursor arrowCursor] set]; whenMouseDown_ = [theEvent timestamp]; actionHasFired_ = NO; if (draggable_) { NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout]; if ([self dragShouldBeginFromMouseDown:theEvent withExpiration:date]) { [self beginDrag:theEvent]; [self endDrag]; } else { if (actsOnMouseDown_) { [self performMouseDownAction:theEvent]; } else { [super mouseDown:theEvent]; } } } else { if (actsOnMouseDown_) { [self performMouseDownAction:theEvent]; } else { [super mouseDown:theEvent]; } } } - (void)beginDrag:(NSEvent*)dragEvent { // Must be overridden by subclasses. NOTREACHED(); } - (void)endDrag { [self highlight:NO]; } @end // @interface DraggableButton