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 <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