Home | History | Annotate | Download | only in cocoa
      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