blob: f64b19f3d225890733f40caabf49b5777aa12291 [file] [log] [blame]
// Copyright (c) 2010 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 "ui/base/cocoa/hover_button.h"
#include <cmath>
namespace {
// Distance to start a drag when a dragDelegate is assigned.
constexpr CGFloat kDragDistance = 5;
} // namespace
@implementation HoverButton
@synthesize hoverState = hoverState_;
@synthesize trackingEnabled = trackingEnabled_;
@synthesize dragDelegate = dragDelegate_;
@synthesize sendActionOnMouseDown = sendActionOnMouseDown_;
- (id)initWithFrame:(NSRect)frameRect {
if ((self = [super initWithFrame:frameRect])) {
[self commonInit];
}
return self;
}
- (void)awakeFromNib {
[self commonInit];
}
- (void)commonInit {
self.hoverState = kHoverStateNone;
self.trackingEnabled = YES;
}
- (void)dealloc {
self.trackingEnabled = NO;
[super dealloc];
}
- (NSRect)hitbox {
return NSZeroRect;
}
- (void)setTrackingEnabled:(BOOL)trackingEnabled {
if (trackingEnabled == trackingEnabled_)
return;
trackingEnabled_ = trackingEnabled;
[self updateTrackingAreas];
}
- (void)setEnabled:(BOOL)enabled {
if (enabled == self.enabled)
return;
super.enabled = enabled;
[self updateTrackingAreas];
}
- (void)mouseEntered:(NSEvent*)theEvent {
if (trackingArea_.get())
self.hoverState = kHoverStateMouseOver;
}
- (void)mouseMoved:(NSEvent*)theEvent {
[self checkImageState];
}
- (void)mouseExited:(NSEvent*)theEvent {
if (trackingArea_.get())
self.hoverState = kHoverStateNone;
}
- (void)mouseDown:(NSEvent*)theEvent {
if (!self.enabled)
return;
mouseDown_ = YES;
self.hoverState = kHoverStateMouseDown;
if (sendActionOnMouseDown_)
[self sendAction:self.action to:self.target];
// The hover button needs to hold onto itself here for a bit. Otherwise,
// it can be freed while in the tracking loop below.
// http://crbug.com/28220
base::scoped_nsobject<HoverButton> myself([self retain]);
// Begin tracking the mouse.
if ([theEvent type] == NSLeftMouseDown) {
NSWindow* window = [self window];
NSEvent* nextEvent = nil;
// For the tracking loop ignore key events so that they don't pile up in
// the queue and get processed after the user releases the mouse.
const NSEventMask eventMask = (NSLeftMouseDraggedMask | NSLeftMouseUpMask |
NSKeyDownMask | NSKeyUpMask);
while ((nextEvent = [window nextEventMatchingMask:eventMask])) {
if ([nextEvent type] == NSLeftMouseUp)
break;
// Update the image state, which will change if the user moves the mouse
// into or out of the button.
[self checkImageState];
if (dragDelegate_ && [nextEvent type] == NSLeftMouseDragged) {
const NSPoint startPos = [theEvent locationInWindow];
const NSPoint pos = [nextEvent locationInWindow];
if (std::abs(startPos.x - pos.x) > kDragDistance ||
std::abs(startPos.y - pos.y) > kDragDistance) {
[dragDelegate_ beginDragFromHoverButton:self event:nextEvent];
mouseDown_ = NO;
self.hoverState = kHoverStateNone;
return;
}
}
}
}
// If the mouse is still over the button, it means the user clicked the
// button.
if (!sendActionOnMouseDown_ && self.hoverState == kHoverStateMouseDown) {
[self sendAction:self.action to:self.target];
}
// Clean up.
mouseDown_ = NO;
[self checkImageState];
}
- (void)setAccessibilityTitle:(NSString*)accessibilityTitle {
NSCell* cell = [self cell];
[cell accessibilitySetOverrideValue:accessibilityTitle
forAttribute:NSAccessibilityTitleAttribute];
}
- (void)updateTrackingAreas {
if (trackingEnabled_ && self.enabled) {
NSRect hitbox = self.hitbox;
if (CrTrackingArea* trackingArea = trackingArea_.get()) {
if (NSEqualRects(trackingArea.rect, hitbox))
return;
[self removeTrackingArea:trackingArea];
}
trackingArea_.reset([[CrTrackingArea alloc]
initWithRect:hitbox
options:NSTrackingMouseEnteredAndExited |
NSTrackingMouseMoved |
NSTrackingActiveAlways |
(NSIsEmptyRect(hitbox) ? NSTrackingInVisibleRect : 0)
owner:self
userInfo:nil]);
[self addTrackingArea:trackingArea_.get()];
// If you have a separate window that overlaps the close button, and you
// move the mouse directly over the close button without entering another
// part of the tab strip, we don't get any mouseEntered event since the
// tracking area was disabled when we entered.
// Done with a delay of 0 because sometimes an event appears to be missed
// between the activation of the tracking area and the call to
// checkImageState resulting in the button state being incorrect.
[self performSelector:@selector(checkImageState)
withObject:nil
afterDelay:0];
} else {
if (trackingArea_.get()) {
self.hoverState = kHoverStateNone;
[self removeTrackingArea:trackingArea_.get()];
trackingArea_.reset(nil);
}
}
[super updateTrackingAreas];
[self checkImageState];
}
- (void)checkImageState {
if (!trackingArea_.get())
return;
NSEvent* currentEvent = [NSApp currentEvent];
if (!currentEvent || currentEvent.window != self.window)
return;
// Update the button's state if the button has moved.
const NSPoint mouseLoc =
[self.superview convertPoint:currentEvent.locationInWindow fromView:nil];
BOOL mouseInBounds = [self hitTest:mouseLoc] != nil;
if (mouseDown_ && mouseInBounds) {
self.hoverState = kHoverStateMouseDown;
} else {
self.hoverState = mouseInBounds ? kHoverStateMouseOver : kHoverStateNone;
}
}
- (void)setHoverState:(HoverState)hoverState {
if (hoverState == hoverState_)
return;
hoverState_ = hoverState;
self.needsDisplay = YES;
}
- (NSView*)hitTest:(NSPoint)point {
if (NSPointInRect([self.superview convertPoint:point toView:self],
self.hitbox)) {
return self;
}
return [super hitTest:point];
}
@end