blob: 25f3fe78ce8731e4eeefacd844a13c77a4d657e7 [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 "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 = 1.0;
}
@implementation DraggableButton
@synthesize draggable = draggable_;
@synthesize actsOnMouseDown = actsOnMouseDown_;
@synthesize durationMouseWasDown = durationMouseWasDown_;
@synthesize actionHasFired = actionHasFired_;
@synthesize whenMouseDown = whenMouseDown_;
- (id)initWithFrame:(NSRect)frame {
if ((self = [super initWithFrame:frame])) {
draggable_ = YES;
actsOnMouseDown_ = NO;
actionHasFired_ = NO;
}
return self;
}
- (id)initWithCoder:(NSCoder*)coder {
if ((self = [super initWithCoder:coder])) {
draggable_ = YES;
actsOnMouseDown_ = NO;
actionHasFired_ = NO;
}
return self;
}
- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis {
return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis);
}
- (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis {
return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis);
}
// 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 = [[self 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;
}
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
withExpiration:(NSDate*)expiration {
return [self dragShouldBeginFromMouseDown:mouseDownEvent
withExpiration:expiration
xHysteresis:kWebDragStartHysteresisX
yHysteresis:kWebDragStartHysteresisY];
}
- (void)mouseUp:(NSEvent*)theEvent {
durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;
if (actionHasFired_)
return;
if (!draggable_) {
[super mouseUp:theEvent];
return;
}
// There are non-drag cases where a mouseUp: may happen
// (e.g. mouse-down, cmd-tab to another application, move mouse,
// mouse-up). So we check.
NSPoint viewLocal = [self convertPoint:[theEvent locationInWindow]
fromView:[[self window] contentView]];
if (NSPointInRect(viewLocal, [self bounds])) {
[self performClick:self];
}
}
- (void)secondaryMouseUpAction:(BOOL)wasInside {
// Override if you want to do any extra work on mouseUp, after a mouseDown
// action has already fired.
}
- (void)performMouseDownAction:(NSEvent*)theEvent {
int eventMask = NSLeftMouseUpMask;
[[self target] performSelector:[self action] withObject:self];
actionHasFired_ = YES;
while (1) {
theEvent = [[self window] nextEventMatchingMask:eventMask];
if (!theEvent)
continue;
NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
fromView:nil];
BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
[self highlight:isInside];
switch ([theEvent type]) {
case NSLeftMouseUp:
durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;
[self secondaryMouseUpAction:isInside];
break;
default:
/* Ignore any other kind of event. */
break;
}
}
[self highlight:NO];
}
// Mimic "begin a click" operation visually. Do NOT follow through
// with normal button event handling.
- (void)mouseDown:(NSEvent*)theEvent {
[[NSCursor arrowCursor] set];
whenMouseDown_ = [theEvent timestamp];
actionHasFired_ = NO;
if (draggable_) {
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout];
if ([self dragShouldBeginFromMouseDown:theEvent
withExpiration:date]) {
[self beginDrag:theEvent];
[self endDrag];
} else {
if (actsOnMouseDown_) {
[self performMouseDownAction:theEvent];
} else {
[super mouseDown:theEvent];
}
}
} else {
if (actsOnMouseDown_) {
[self performMouseDownAction:theEvent];
} else {
[super mouseDown:theEvent];
}
}
}
- (void)beginDrag:(NSEvent*)dragEvent {
// Must be overridden by subclasses.
NOTREACHED();
}
- (void)endDrag {
[self highlight:NO];
}
@end // @interface DraggableButton