blob: e28f674094255d9d81c95718a09ed152109f5045 [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/draggable_button.h"
#include <cmath>
#include "base/logging.h"
#import "base/memory/scoped_nsobject.h"
namespace {
// Code taken from <http://codereview.chromium.org/180036/diff/3001/3004>.
// TODO(viettrungluu): Do we want common, standard code for drag hysteresis?
const CGFloat kWebDragStartHysteresisX = 5.0;
const CGFloat kWebDragStartHysteresisY = 5.0;
const CGFloat kDragExpirationTimeout = 0.45;
}
// Private /////////////////////////////////////////////////////////////////////
@interface DraggableButtonImpl (Private)
- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis;
- (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis;
- (void)performMouseDownAction:(NSEvent*)theEvent;
- (void)endDrag;
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
withExpiration:(NSDate*)expiration;
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
withExpiration:(NSDate*)expiration
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis;
@end
// Implementation //////////////////////////////////////////////////////////////
@implementation DraggableButtonImpl
@synthesize draggable = draggable_;
@synthesize actsOnMouseDown = actsOnMouseDown_;
@synthesize durationMouseWasDown = durationMouseWasDown_;
@synthesize actionHasFired = actionHasFired_;
@synthesize whenMouseDown = whenMouseDown_;
- (id)initWithButton:(NSButton<DraggableButtonMixin>*)button {
if ((self = [super init])) {
button_ = button;
draggable_ = YES;
actsOnMouseDown_ = NO;
actionHasFired_ = NO;
}
return self;
}
// NSButton/NSResponder Implementations ////////////////////////////////////////
- (DraggableButtonResult)mouseUpImpl:(NSEvent*)theEvent {
durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;
if (actionHasFired_)
return kDraggableButtonImplDidWork;
if (!draggable_)
return kDraggableButtonMixinCallSuper;
// There are non-drag cases where a |-mouseUp:| may happen (e.g. mouse-down,
// cmd-tab to another application, move mouse, mouse-up), so check.
NSPoint viewLocal = [button_ convertPoint:[theEvent locationInWindow]
fromView:[[button_ window] contentView]];
if (NSPointInRect(viewLocal, [button_ bounds]))
[button_ performClick:self];
return kDraggableButtonImplDidWork;
}
// Mimic "begin a click" operation visually. Do NOT follow through with normal
// button event handling.
- (DraggableButtonResult)mouseDownImpl:(NSEvent*)theEvent {
[[NSCursor arrowCursor] set];
whenMouseDown_ = [theEvent timestamp];
actionHasFired_ = NO;
if (draggable_) {
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout];
if ([self dragShouldBeginFromMouseDown:theEvent
withExpiration:date]) {
[button_ beginDrag:theEvent];
[self endDrag];
} else {
if (actsOnMouseDown_) {
[self performMouseDownAction:theEvent];
return kDraggableButtonImplDidWork;
} else {
return kDraggableButtonMixinCallSuper;
}
}
} else {
if (actsOnMouseDown_) {
[self performMouseDownAction:theEvent];
return kDraggableButtonImplDidWork;
} else {
return kDraggableButtonMixinCallSuper;
}
}
return kDraggableButtonImplDidWork;
}
// Idempotent Helpers //////////////////////////////////////////////////////////
- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis {
if ([button_ respondsToSelector:@selector(deltaIndicatesDragStartWithXDelta:
yDelta:
xHysteresis:
yHysteresis:
indicates:)]) {
BOOL indicates = NO;
DraggableButtonResult result = [button_
deltaIndicatesDragStartWithXDelta:xDelta
yDelta:yDelta
xHysteresis:xHysteresis
yHysteresis:yHysteresis
indicates:&indicates];
if (result != kDraggableButtonImplUseBase)
return indicates;
}
return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
}
- (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis {
if ([button_ respondsToSelector:
@selector(deltaIndicatesConclusionReachedWithXDelta:
yDelta:
xHysteresis:
yHysteresis:
indicates:)]) {
BOOL indicates = NO;
DraggableButtonResult result = [button_
deltaIndicatesConclusionReachedWithXDelta:xDelta
yDelta:yDelta
xHysteresis:xHysteresis
yHysteresis:yHysteresis
indicates:&indicates];
if (result != kDraggableButtonImplUseBase)
return indicates;
}
return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
}
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
withExpiration:(NSDate*)expiration {
return [self dragShouldBeginFromMouseDown:mouseDownEvent
withExpiration:expiration
xHysteresis:kWebDragStartHysteresisX
yHysteresis:kWebDragStartHysteresisY];
}
// Implementation Details //////////////////////////////////////////////////////
// Determine whether a mouse down should turn into a drag; started as copy of
// NSTableView code.
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
withExpiration:(NSDate*)expiration
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis {
if ([mouseDownEvent type] != NSLeftMouseDown) {
return NO;
}
NSEvent* nextEvent = nil;
NSEvent* firstEvent = nil;
NSEvent* dragEvent = nil;
NSEvent* mouseUp = nil;
BOOL dragIt = NO;
while ((nextEvent = [[button_ window]
nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
untilDate:expiration
inMode:NSEventTrackingRunLoopMode
dequeue:YES]) != nil) {
if (firstEvent == nil) {
firstEvent = nextEvent;
}
if ([nextEvent type] == NSLeftMouseDragged) {
float deltax = [nextEvent locationInWindow].x -
[mouseDownEvent locationInWindow].x;
float deltay = [nextEvent locationInWindow].y -
[mouseDownEvent locationInWindow].y;
dragEvent = nextEvent;
if ([self deltaIndicatesConclusionReachedWithXDelta:deltax
yDelta:deltay
xHysteresis:xHysteresis
yHysteresis:yHysteresis]) {
dragIt = [self deltaIndicatesDragStartWithXDelta:deltax
yDelta:deltay
xHysteresis:xHysteresis
yHysteresis:yHysteresis];
break;
}
} else if ([nextEvent type] == NSLeftMouseUp) {
mouseUp = nextEvent;
break;
}
}
// Since we've been dequeuing the events (If we don't, we'll never see
// the mouse up...), we need to push some of the events back on.
// It makes sense to put the first and last drag events and the mouse
// up if there was one.
if (mouseUp != nil) {
[NSApp postEvent:mouseUp atStart:YES];
}
if (dragEvent != nil) {
[NSApp postEvent:dragEvent atStart:YES];
}
if (firstEvent != mouseUp && firstEvent != dragEvent) {
[NSApp postEvent:firstEvent atStart:YES];
}
return dragIt;
}
- (void)secondaryMouseUpAction:(BOOL)wasInside {
if ([button_ respondsToSelector:_cmd])
[button_ secondaryMouseUpAction:wasInside];
// No actual implementation yet.
}
- (void)performMouseDownAction:(NSEvent*)event {
if ([button_ respondsToSelector:_cmd] &&
[button_ performMouseDownAction:event] != kDraggableButtonImplUseBase) {
return;
}
int eventMask = NSLeftMouseUpMask;
[[button_ target] performSelector:[button_ action] withObject:self];
actionHasFired_ = YES;
while (1) {
event = [[button_ window] nextEventMatchingMask:eventMask];
if (!event)
continue;
NSPoint mouseLoc = [button_ convertPoint:[event locationInWindow]
fromView:nil];
BOOL isInside = [button_ mouse:mouseLoc inRect:[button_ bounds]];
[button_ highlight:isInside];
switch ([event type]) {
case NSLeftMouseUp:
durationMouseWasDown_ = [event timestamp] - whenMouseDown_;
[self secondaryMouseUpAction:isInside];
break;
default:
// Ignore any other kind of event.
break;
}
}
[button_ highlight:NO];
}
- (void)endDrag {
if ([button_ respondsToSelector:_cmd] &&
[button_ endDrag] != kDraggableButtonImplUseBase) {
return;
}
[button_ highlight:NO];
}
@end