1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/draggable_button.h"
9 #include "base/logging.h"
11 // Code taken from <http://codereview.chromium.org/180036/diff/3001/3004>.
12 // TODO(viettrungluu): Do we want common, standard code for drag hysteresis?
13 const CGFloat kWebDragStartHysteresisX = 5.0;
14 const CGFloat kWebDragStartHysteresisY = 5.0;
15 const CGFloat kDragExpirationTimeout = 0.45;
17 // Private /////////////////////////////////////////////////////////////////////
19 @interface DraggableButtonImpl (Private)
21 - (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
23 xHysteresis:(float)xHysteresis
24 yHysteresis:(float)yHysteresis;
25 - (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
27 xHysteresis:(float)xHysteresis
28 yHysteresis:(float)yHysteresis;
29 - (void)performMouseDownAction:(NSEvent*)theEvent;
31 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
32 withExpiration:(NSDate*)expiration;
33 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
34 withExpiration:(NSDate*)expiration
35 xHysteresis:(float)xHysteresis
36 yHysteresis:(float)yHysteresis;
40 // Implementation //////////////////////////////////////////////////////////////
42 @implementation DraggableButtonImpl
44 @synthesize draggable = draggable_;
45 @synthesize actsOnMouseDown = actsOnMouseDown_;
46 @synthesize durationMouseWasDown = durationMouseWasDown_;
47 @synthesize actionHasFired = actionHasFired_;
48 @synthesize whenMouseDown = whenMouseDown_;
50 - (id)initWithButton:(NSButton<DraggableButtonMixin>*)button {
51 if ((self = [super init])) {
54 actsOnMouseDown_ = NO;
60 // NSButton/NSResponder Implementations ////////////////////////////////////////
62 - (DraggableButtonResult)mouseUpImpl:(NSEvent*)theEvent {
63 durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;
66 return kDraggableButtonImplDidWork;
69 return kDraggableButtonMixinCallSuper;
71 // There are non-drag cases where a |-mouseUp:| may happen (e.g. mouse-down,
72 // cmd-tab to another application, move mouse, mouse-up), so check.
73 NSPoint viewLocal = [button_ convertPoint:[theEvent locationInWindow]
74 fromView:[[button_ window] contentView]];
75 if (NSPointInRect(viewLocal, [button_ bounds]))
76 [button_ performClick:self];
78 return kDraggableButtonImplDidWork;
81 // Mimic "begin a click" operation visually. Do NOT follow through with normal
82 // button event handling.
83 - (DraggableButtonResult)mouseDownImpl:(NSEvent*)theEvent {
84 [[NSCursor arrowCursor] set];
86 whenMouseDown_ = [theEvent timestamp];
90 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout];
91 if ([self dragShouldBeginFromMouseDown:theEvent
92 withExpiration:date]) {
93 [button_ beginDrag:theEvent];
96 if (actsOnMouseDown_) {
97 [self performMouseDownAction:theEvent];
98 return kDraggableButtonImplDidWork;
100 return kDraggableButtonMixinCallSuper;
104 if (actsOnMouseDown_) {
105 [self performMouseDownAction:theEvent];
106 return kDraggableButtonImplDidWork;
108 return kDraggableButtonMixinCallSuper;
112 return kDraggableButtonImplDidWork;
115 // Idempotent Helpers //////////////////////////////////////////////////////////
117 - (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
119 xHysteresis:(float)xHysteresis
120 yHysteresis:(float)yHysteresis {
121 if ([button_ respondsToSelector:@selector(deltaIndicatesDragStartWithXDelta:
127 DraggableButtonResult result = [button_
128 deltaIndicatesDragStartWithXDelta:xDelta
130 xHysteresis:xHysteresis
131 yHysteresis:yHysteresis
132 indicates:&indicates];
133 if (result != kDraggableButtonImplUseBase)
136 return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
139 - (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
141 xHysteresis:(float)xHysteresis
142 yHysteresis:(float)yHysteresis {
143 if ([button_ respondsToSelector:
144 @selector(deltaIndicatesConclusionReachedWithXDelta:
150 DraggableButtonResult result = [button_
151 deltaIndicatesConclusionReachedWithXDelta:xDelta
153 xHysteresis:xHysteresis
154 yHysteresis:yHysteresis
155 indicates:&indicates];
156 if (result != kDraggableButtonImplUseBase)
159 return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
162 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
163 withExpiration:(NSDate*)expiration {
164 return [self dragShouldBeginFromMouseDown:mouseDownEvent
165 withExpiration:expiration
166 xHysteresis:kWebDragStartHysteresisX
167 yHysteresis:kWebDragStartHysteresisY];
170 // Implementation Details //////////////////////////////////////////////////////
172 // Determine whether a mouse down should turn into a drag; started as copy of
174 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
175 withExpiration:(NSDate*)expiration
176 xHysteresis:(float)xHysteresis
177 yHysteresis:(float)yHysteresis {
178 if ([mouseDownEvent type] != NSLeftMouseDown) {
182 NSEvent* nextEvent = nil;
183 NSEvent* firstEvent = nil;
184 NSEvent* dragEvent = nil;
185 NSEvent* mouseUp = nil;
188 while ((nextEvent = [[button_ window]
189 nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
191 inMode:NSEventTrackingRunLoopMode
192 dequeue:YES]) != nil) {
193 if (firstEvent == nil) {
194 firstEvent = nextEvent;
196 if ([nextEvent type] == NSLeftMouseDragged) {
197 float deltax = [nextEvent locationInWindow].x -
198 [mouseDownEvent locationInWindow].x;
199 float deltay = [nextEvent locationInWindow].y -
200 [mouseDownEvent locationInWindow].y;
201 dragEvent = nextEvent;
202 if ([self deltaIndicatesConclusionReachedWithXDelta:deltax
204 xHysteresis:xHysteresis
205 yHysteresis:yHysteresis]) {
206 dragIt = [self deltaIndicatesDragStartWithXDelta:deltax
208 xHysteresis:xHysteresis
209 yHysteresis:yHysteresis];
212 } else if ([nextEvent type] == NSLeftMouseUp) {
218 // Since we've been dequeuing the events (If we don't, we'll never see
219 // the mouse up...), we need to push some of the events back on.
220 // It makes sense to put the first and last drag events and the mouse
221 // up if there was one.
222 if (mouseUp != nil) {
223 [NSApp postEvent:mouseUp atStart:YES];
225 if (dragEvent != nil) {
226 [NSApp postEvent:dragEvent atStart:YES];
228 if (firstEvent != mouseUp && firstEvent != dragEvent) {
229 [NSApp postEvent:firstEvent atStart:YES];
235 - (void)secondaryMouseUpAction:(BOOL)wasInside {
236 if ([button_ respondsToSelector:_cmd])
237 [button_ secondaryMouseUpAction:wasInside];
239 // No actual implementation yet.
242 - (void)performMouseDownAction:(NSEvent*)event {
243 if ([button_ respondsToSelector:_cmd] &&
244 [button_ performMouseDownAction:event] != kDraggableButtonImplUseBase) {
248 int eventMask = NSLeftMouseUpMask;
250 [[button_ target] performSelector:[button_ action] withObject:self];
251 actionHasFired_ = YES;
254 event = [[button_ window] nextEventMatchingMask:eventMask];
257 NSPoint mouseLoc = [button_ convertPoint:[event locationInWindow]
259 BOOL isInside = [button_ mouse:mouseLoc inRect:[button_ bounds]];
260 [button_ highlight:isInside];
262 switch ([event type]) {
264 durationMouseWasDown_ = [event timestamp] - whenMouseDown_;
265 [self secondaryMouseUpAction:isInside];
268 // Ignore any other kind of event.
273 [button_ highlight:NO];
277 if ([button_ respondsToSelector:_cmd] &&
278 [button_ endDrag] != kDraggableButtonImplUseBase) {
281 [button_ highlight:NO];