• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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