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. 4 5 #import "chrome/browser/ui/cocoa/draggable_button.h" 6 7 #include <cmath> 8 9 #include "base/logging.h" 10 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; 16 17 // Private ///////////////////////////////////////////////////////////////////// 18 19 @interface DraggableButtonImpl (Private) 20 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; 37 38 @end 39 40 // Implementation ////////////////////////////////////////////////////////////// 41 42 @implementation DraggableButtonImpl 43 44 @synthesize draggable = draggable_; 45 @synthesize actsOnMouseDown = actsOnMouseDown_; 46 @synthesize durationMouseWasDown = durationMouseWasDown_; 47 @synthesize actionHasFired = actionHasFired_; 48 @synthesize whenMouseDown = whenMouseDown_; 49 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; 58 } 59 60 // NSButton/NSResponder Implementations //////////////////////////////////////// 61 62 - (DraggableButtonResult)mouseUpImpl:(NSEvent*)theEvent { 63 durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_; 64 65 if (actionHasFired_) 66 return kDraggableButtonImplDidWork; 67 68 if (!draggable_) 69 return kDraggableButtonMixinCallSuper; 70 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]; 77 78 return kDraggableButtonImplDidWork; 79 } 80 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]; 85 86 whenMouseDown_ = [theEvent timestamp]; 87 actionHasFired_ = NO; 88 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 } 111 112 return kDraggableButtonImplDidWork; 113 } 114 115 // Idempotent Helpers ////////////////////////////////////////////////////////// 116 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); 137 } 138 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); 160 } 161 162 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent 163 withExpiration:(NSDate*)expiration { 164 return [self dragShouldBeginFromMouseDown:mouseDownEvent 165 withExpiration:expiration 166 xHysteresis:kWebDragStartHysteresisX 167 yHysteresis:kWebDragStartHysteresisY]; 168 } 169 170 // Implementation Details ////////////////////////////////////////////////////// 171 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 } 181 182 NSEvent* nextEvent = nil; 183 NSEvent* firstEvent = nil; 184 NSEvent* dragEvent = nil; 185 NSEvent* mouseUp = nil; 186 BOOL dragIt = NO; 187 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 } 217 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 } 231 232 return dragIt; 233 } 234 235 - (void)secondaryMouseUpAction:(BOOL)wasInside { 236 if ([button_ respondsToSelector:_cmd]) 237 [button_ secondaryMouseUpAction:wasInside]; 238 239 // No actual implementation yet. 240 } 241 242 - (void)performMouseDownAction:(NSEvent*)event { 243 if ([button_ respondsToSelector:_cmd] && 244 [button_ performMouseDownAction:event] != kDraggableButtonImplUseBase) { 245 return; 246 } 247 248 int eventMask = NSLeftMouseUpMask; 249 250 [[button_ target] performSelector:[button_ action] withObject:self]; 251 actionHasFired_ = YES; 252 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]; 261 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 } 272 273 [button_ highlight:NO]; 274 } 275 276 - (void)endDrag { 277 if ([button_ respondsToSelector:_cmd] && 278 [button_ endDrag] != kDraggableButtonImplUseBase) { 279 return; 280 } 281 [button_ highlight:NO]; 282 } 283 284 @end 285