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 "base/logging.h" 8 #import "base/memory/scoped_nsobject.h" 9 10 namespace { 11 12 // Code taken from <http://codereview.chromium.org/180036/diff/3001/3004>. 13 // TODO(viettrungluu): Do we want common, standard code for drag hysteresis? 14 const CGFloat kWebDragStartHysteresisX = 5.0; 15 const CGFloat kWebDragStartHysteresisY = 5.0; 16 const CGFloat kDragExpirationTimeout = 1.0; 17 18 } 19 20 @implementation DraggableButton 21 22 @synthesize draggable = draggable_; 23 @synthesize actsOnMouseDown = actsOnMouseDown_; 24 @synthesize durationMouseWasDown = durationMouseWasDown_; 25 @synthesize actionHasFired = actionHasFired_; 26 @synthesize whenMouseDown = whenMouseDown_; 27 28 29 - (id)initWithFrame:(NSRect)frame { 30 if ((self = [super initWithFrame:frame])) { 31 draggable_ = YES; 32 actsOnMouseDown_ = NO; 33 actionHasFired_ = NO; 34 } 35 return self; 36 } 37 38 - (id)initWithCoder:(NSCoder*)coder { 39 if ((self = [super initWithCoder:coder])) { 40 draggable_ = YES; 41 actsOnMouseDown_ = NO; 42 actionHasFired_ = NO; 43 } 44 return self; 45 } 46 47 - (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta 48 yDelta:(float)yDelta 49 xHysteresis:(float)xHysteresis 50 yHysteresis:(float)yHysteresis { 51 return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis); 52 } 53 54 - (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta 55 yDelta:(float)yDelta 56 xHysteresis:(float)xHysteresis 57 yHysteresis:(float)yHysteresis { 58 return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis); 59 } 60 61 62 // Determine whether a mouse down should turn into a drag; started as copy of 63 // NSTableView code. 64 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent 65 withExpiration:(NSDate*)expiration 66 xHysteresis:(float)xHysteresis 67 yHysteresis:(float)yHysteresis { 68 if ([mouseDownEvent type] != NSLeftMouseDown) { 69 return NO; 70 } 71 72 NSEvent* nextEvent = nil; 73 NSEvent* firstEvent = nil; 74 NSEvent* dragEvent = nil; 75 NSEvent* mouseUp = nil; 76 BOOL dragIt = NO; 77 78 while ((nextEvent = [[self window] 79 nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask) 80 untilDate:expiration 81 inMode:NSEventTrackingRunLoopMode 82 dequeue:YES]) != nil) { 83 if (firstEvent == nil) { 84 firstEvent = nextEvent; 85 } 86 if ([nextEvent type] == NSLeftMouseDragged) { 87 float deltax = [nextEvent locationInWindow].x - 88 [mouseDownEvent locationInWindow].x; 89 float deltay = [nextEvent locationInWindow].y - 90 [mouseDownEvent locationInWindow].y; 91 dragEvent = nextEvent; 92 if ([self deltaIndicatesConclusionReachedWithXDelta:deltax 93 yDelta:deltay 94 xHysteresis:xHysteresis 95 yHysteresis:yHysteresis]) { 96 dragIt = [self deltaIndicatesDragStartWithXDelta:deltax 97 yDelta:deltay 98 xHysteresis:xHysteresis 99 yHysteresis:yHysteresis]; 100 break; 101 } 102 } else if ([nextEvent type] == NSLeftMouseUp) { 103 mouseUp = nextEvent; 104 break; 105 } 106 } 107 108 // Since we've been dequeuing the events (If we don't, we'll never see 109 // the mouse up...), we need to push some of the events back on. 110 // It makes sense to put the first and last drag events and the mouse 111 // up if there was one. 112 if (mouseUp != nil) { 113 [NSApp postEvent:mouseUp atStart:YES]; 114 } 115 if (dragEvent != nil) { 116 [NSApp postEvent:dragEvent atStart:YES]; 117 } 118 if (firstEvent != mouseUp && firstEvent != dragEvent) { 119 [NSApp postEvent:firstEvent atStart:YES]; 120 } 121 122 return dragIt; 123 } 124 125 - (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent 126 withExpiration:(NSDate*)expiration { 127 return [self dragShouldBeginFromMouseDown:mouseDownEvent 128 withExpiration:expiration 129 xHysteresis:kWebDragStartHysteresisX 130 yHysteresis:kWebDragStartHysteresisY]; 131 } 132 133 - (void)mouseUp:(NSEvent*)theEvent { 134 durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_; 135 136 if (actionHasFired_) 137 return; 138 139 if (!draggable_) { 140 [super mouseUp:theEvent]; 141 return; 142 } 143 144 // There are non-drag cases where a mouseUp: may happen 145 // (e.g. mouse-down, cmd-tab to another application, move mouse, 146 // mouse-up). So we check. 147 NSPoint viewLocal = [self convertPoint:[theEvent locationInWindow] 148 fromView:[[self window] contentView]]; 149 if (NSPointInRect(viewLocal, [self bounds])) { 150 [self performClick:self]; 151 } 152 } 153 154 - (void)secondaryMouseUpAction:(BOOL)wasInside { 155 // Override if you want to do any extra work on mouseUp, after a mouseDown 156 // action has already fired. 157 } 158 159 - (void)performMouseDownAction:(NSEvent*)theEvent { 160 int eventMask = NSLeftMouseUpMask; 161 162 [[self target] performSelector:[self action] withObject:self]; 163 actionHasFired_ = YES; 164 165 while (1) { 166 theEvent = [[self window] nextEventMatchingMask:eventMask]; 167 if (!theEvent) 168 continue; 169 NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] 170 fromView:nil]; 171 BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]]; 172 [self highlight:isInside]; 173 174 switch ([theEvent type]) { 175 case NSLeftMouseUp: 176 durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_; 177 [self secondaryMouseUpAction:isInside]; 178 break; 179 default: 180 /* Ignore any other kind of event. */ 181 break; 182 } 183 } 184 185 [self highlight:NO]; 186 } 187 188 // Mimic "begin a click" operation visually. Do NOT follow through 189 // with normal button event handling. 190 - (void)mouseDown:(NSEvent*)theEvent { 191 [[NSCursor arrowCursor] set]; 192 193 whenMouseDown_ = [theEvent timestamp]; 194 actionHasFired_ = NO; 195 196 if (draggable_) { 197 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout]; 198 if ([self dragShouldBeginFromMouseDown:theEvent 199 withExpiration:date]) { 200 [self beginDrag:theEvent]; 201 [self endDrag]; 202 } else { 203 if (actsOnMouseDown_) { 204 [self performMouseDownAction:theEvent]; 205 } else { 206 [super mouseDown:theEvent]; 207 } 208 209 } 210 } else { 211 if (actsOnMouseDown_) { 212 [self performMouseDownAction:theEvent]; 213 } else { 214 [super mouseDown:theEvent]; 215 } 216 } 217 } 218 219 - (void)beginDrag:(NSEvent*)dragEvent { 220 // Must be overridden by subclasses. 221 NOTREACHED(); 222 } 223 224 - (void)endDrag { 225 [self highlight:NO]; 226 } 227 228 @end // @interface DraggableButton 229