Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / draggable_button_mixin.mm
blob986f5e92b70c57455e5bfd4040c1454e32c13585
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"
7 #include <cmath>
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
22                                    yDelta:(float)yDelta
23                               xHysteresis:(float)xHysteresis
24                               yHysteresis:(float)yHysteresis;
25 - (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
26                                            yDelta:(float)yDelta
27                                       xHysteresis:(float)xHysteresis
28                                       yHysteresis:(float)yHysteresis;
29 - (void)performMouseDownAction:(NSEvent*)theEvent;
30 - (void)endDrag;
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;
38 @end
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])) {
52     button_ = button;
53     draggable_ = YES;
54     actsOnMouseDown_ = NO;
55     actionHasFired_ = NO;
56   }
57   return self;
60 // NSButton/NSResponder Implementations ////////////////////////////////////////
62 - (DraggableButtonResult)mouseUpImpl:(NSEvent*)theEvent {
63   durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;
65   if (actionHasFired_)
66     return kDraggableButtonImplDidWork;
68   if (!draggable_)
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];
87   actionHasFired_ = NO;
89   if (draggable_) {
90     NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout];
91     if ([self dragShouldBeginFromMouseDown:theEvent
92                             withExpiration:date]) {
93       [button_ beginDrag:theEvent];
94       [self endDrag];
95     } else {
96       if (actsOnMouseDown_) {
97         [self performMouseDownAction:theEvent];
98         return kDraggableButtonImplDidWork;
99       } else {
100         return kDraggableButtonMixinCallSuper;
101       }
102     }
103   } else {
104     if (actsOnMouseDown_) {
105       [self performMouseDownAction:theEvent];
106       return kDraggableButtonImplDidWork;
107     } else {
108       return kDraggableButtonMixinCallSuper;
109     }
110   }
112   return kDraggableButtonImplDidWork;
115 // Idempotent Helpers //////////////////////////////////////////////////////////
117 - (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
118                                    yDelta:(float)yDelta
119                               xHysteresis:(float)xHysteresis
120                               yHysteresis:(float)yHysteresis {
121   if ([button_ respondsToSelector:@selector(deltaIndicatesDragStartWithXDelta:
122                                             yDelta:
123                                             xHysteresis:
124                                             yHysteresis:
125                                             indicates:)]) {
126     BOOL indicates = NO;
127     DraggableButtonResult result = [button_
128         deltaIndicatesDragStartWithXDelta:xDelta
129         yDelta:yDelta
130         xHysteresis:xHysteresis
131         yHysteresis:yHysteresis
132         indicates:&indicates];
133     if (result != kDraggableButtonImplUseBase)
134       return indicates;
135   }
136   return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
139 - (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
140                                            yDelta:(float)yDelta
141                                       xHysteresis:(float)xHysteresis
142                                       yHysteresis:(float)yHysteresis {
143   if ([button_ respondsToSelector:
144           @selector(deltaIndicatesConclusionReachedWithXDelta:
145                     yDelta:
146                     xHysteresis:
147                     yHysteresis:
148                     indicates:)]) {
149     BOOL indicates = NO;
150     DraggableButtonResult result = [button_
151         deltaIndicatesConclusionReachedWithXDelta:xDelta
152         yDelta:yDelta
153         xHysteresis:xHysteresis
154         yHysteresis:yHysteresis
155         indicates:&indicates];
156     if (result != kDraggableButtonImplUseBase)
157       return indicates;
158   }
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
173 // NSTableView code.
174 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
175                       withExpiration:(NSDate*)expiration
176                          xHysteresis:(float)xHysteresis
177                          yHysteresis:(float)yHysteresis {
178   if ([mouseDownEvent type] != NSLeftMouseDown) {
179     return NO;
180   }
182   NSEvent* nextEvent = nil;
183   NSEvent* firstEvent = nil;
184   NSEvent* dragEvent = nil;
185   NSEvent* mouseUp = nil;
186   BOOL dragIt = NO;
188   while ((nextEvent = [[button_ window]
189       nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
190                   untilDate:expiration
191                      inMode:NSEventTrackingRunLoopMode
192                     dequeue:YES]) != nil) {
193     if (firstEvent == nil) {
194       firstEvent = nextEvent;
195     }
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
203                                                    yDelta:deltay
204                                               xHysteresis:xHysteresis
205                                               yHysteresis:yHysteresis]) {
206         dragIt = [self deltaIndicatesDragStartWithXDelta:deltax
207                                                   yDelta:deltay
208                                              xHysteresis:xHysteresis
209                                              yHysteresis:yHysteresis];
210         break;
211       }
212     } else if ([nextEvent type] == NSLeftMouseUp) {
213       mouseUp = nextEvent;
214       break;
215     }
216   }
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];
224   }
225   if (dragEvent != nil) {
226     [NSApp postEvent:dragEvent atStart:YES];
227   }
228   if (firstEvent != mouseUp && firstEvent != dragEvent) {
229     [NSApp postEvent:firstEvent atStart:YES];
230   }
232   return dragIt;
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) {
245       return;
246   }
248   int eventMask = NSLeftMouseUpMask;
250   [[button_ target] performSelector:[button_ action] withObject:self];
251   actionHasFired_ = YES;
253   while (1) {
254     event = [[button_ window] nextEventMatchingMask:eventMask];
255     if (!event)
256       continue;
257     NSPoint mouseLoc = [button_ convertPoint:[event locationInWindow]
258                                     fromView:nil];
259     BOOL isInside = [button_ mouse:mouseLoc inRect:[button_ bounds]];
260     [button_ highlight:isInside];
262     switch ([event type]) {
263       case NSLeftMouseUp:
264         durationMouseWasDown_ = [event timestamp] - whenMouseDown_;
265         [self secondaryMouseUpAction:isInside];
266         break;
267       default:
268         // Ignore any other kind of event.
269         break;
270     }
271   }
273   [button_ highlight:NO];
276 - (void)endDrag {
277   if ([button_ respondsToSelector:_cmd] &&
278       [button_ endDrag] != kDraggableButtonImplUseBase) {
279     return;
280   }
281   [button_ highlight:NO];
284 @end