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 10namespace { 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? 14const CGFloat kWebDragStartHysteresisX = 5.0; 15const CGFloat kWebDragStartHysteresisY = 5.0; 16const 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