blob: 13da74cadb6b4b31e6de272fc0a89c8bdf4b6a77 [file] [log] [blame]
// Copyright (c) 2012 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/bookmarks/bookmark_button.h"
#include <cmath>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#import "base/mac/scoped_nsobject.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/nsview_additions.h"
#import "chrome/browser/ui/cocoa/view_id_util.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "content/public/browser/user_metrics.h"
#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
using base::UserMetricsAction;
// The opacity of the bookmark button drag image.
static const CGFloat kDragImageOpacity = 0.7;
namespace bookmark_button {
NSString* const kPulseBookmarkButtonNotification =
@"PulseBookmarkButtonNotification";
NSString* const kBookmarkKey = @"BookmarkKey";
NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
};
namespace {
// We need a class variable to track the current dragged button to enable
// proper live animated dragging behavior, and can't do it in the
// delegate/controller since you can drag a button from one domain to the
// other (from a "folder" menu, to the main bar, or vice versa).
BookmarkButton* gDraggedButton = nil; // Weak
};
@interface BookmarkButton(Private)
// Make a drag image for the button.
- (NSImage*)dragImage;
- (void)installCustomTrackingArea;
@end // @interface BookmarkButton(Private)
@implementation BookmarkButton
@synthesize delegate = delegate_;
@synthesize acceptsTrackIn = acceptsTrackIn_;
- (id)initWithFrame:(NSRect)frameRect {
// BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
// BookmarkBarController, so we can't just override -viewID method to return
// it.
if ((self = [super initWithFrame:frameRect])) {
view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
[self installCustomTrackingArea];
}
return self;
}
- (void)dealloc {
if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
[[self cell] safelyStopPulsing];
view_id_util::UnsetID(self);
if (area_) {
[self removeTrackingArea:area_];
[area_ release];
}
[super dealloc];
}
- (const BookmarkNode*)bookmarkNode {
return [[self cell] bookmarkNode];
}
- (BOOL)isFolder {
const BookmarkNode* node = [self bookmarkNode];
return (node && node->is_folder());
}
- (BOOL)isEmpty {
return [self bookmarkNode] ? NO : YES;
}
- (void)setIsContinuousPulsing:(BOOL)flag {
[[self cell] setIsContinuousPulsing:flag];
}
- (BOOL)isContinuousPulsing {
return [[self cell] isContinuousPulsing];
}
- (NSPoint)screenLocationForRemoveAnimation {
NSPoint point;
if (dragPending_) {
// Use the position of the mouse in the drag image as the location.
point = dragEndScreenLocation_;
point.x += dragMouseOffset_.x;
if ([self isFlipped]) {
point.y += [self bounds].size.height - dragMouseOffset_.y;
} else {
point.y += dragMouseOffset_.y;
}
} else {
// Use the middle of this button as the location.
NSRect bounds = [self bounds];
point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
point = [self convertPoint:point toView:nil];
point = [[self window] convertBaseToScreen:point];
}
return point;
}
- (void)updateTrackingAreas {
[self installCustomTrackingArea];
[super updateTrackingAreas];
}
- (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
yDelta:(float)yDelta
xHysteresis:(float)xHysteresis
yHysteresis:(float)yHysteresis
indicates:(BOOL*)result {
const float kDownProportion = 1.4142135f; // Square root of 2.
// We want to show a folder menu when you drag down on folder buttons,
// so don't classify this as a drag for that case.
if ([self isFolder] &&
(yDelta <= -yHysteresis) && // Bottom of hysteresis box was hit.
(std::abs(yDelta) / std::abs(xDelta)) >= kDownProportion) {
*result = NO;
return kDraggableButtonMixinDidWork;
}
return kDraggableButtonImplUseBase;
}
// By default, NSButton ignores middle-clicks.
// But we want them.
- (void)otherMouseUp:(NSEvent*)event {
[self performClick:self];
}
- (BOOL)acceptsTrackInFrom:(id)sender {
return [self isFolder] || [self acceptsTrackIn];
}
// Overridden from DraggableButton.
- (void)beginDrag:(NSEvent*)event {
// Don't allow a drag of the empty node.
// The empty node is a placeholder for "(empty)", to be revisited.
if ([self isEmpty])
return;
if (![self delegate]) {
NOTREACHED();
return;
}
if ([self isFolder]) {
// Close the folder's drop-down menu if it's visible.
[[self target] closeBookmarkFolder:self];
}
// At the moment, moving bookmarks causes their buttons (like me!)
// to be destroyed and rebuilt. Make sure we don't go away while on
// the stack.
[self retain];
// Ask our delegate to fill the pasteboard for us.
NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[[self delegate] fillPasteboard:pboard forDragOfButton:self];
// Lock bar visibility, forcing the overlay to stay visible if we are in
// fullscreen mode.
if ([[self delegate] dragShouldLockBarVisibility]) {
DCHECK(!visibilityDelegate_);
NSWindow* window = [[self delegate] browserWindow];
visibilityDelegate_ =
[BrowserWindowController browserWindowControllerForWindow:window];
[visibilityDelegate_ lockBarVisibilityForOwner:self
withAnimation:NO
delay:NO];
}
const BookmarkNode* node = [self bookmarkNode];
const BookmarkNode* parent = node ? node->parent() : NULL;
if (parent && parent->type() == BookmarkNode::FOLDER) {
content::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
} else {
content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
}
dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
dragPending_ = YES;
gDraggedButton = self;
CGFloat yAt = [self bounds].size.height;
NSSize dragOffset = NSMakeSize(0.0, 0.0);
NSImage* image = [self dragImage];
[self setHidden:YES];
[self dragImage:image at:NSMakePoint(0, yAt) offset:dragOffset
event:event pasteboard:pboard source:self slideBack:YES];
[self setHidden:NO];
// And we're done.
dragPending_ = NO;
gDraggedButton = nil;
[self autorelease];
}
// Overridden to release bar visibility.
- (DraggableButtonResult)endDrag {
gDraggedButton = nil;
// visibilityDelegate_ can be nil if we're detached, and that's fine.
[visibilityDelegate_ releaseBarVisibilityForOwner:self
withAnimation:YES
delay:YES];
visibilityDelegate_ = nil;
return kDraggableButtonImplUseBase;
}
- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
NSDragOperation operation = NSDragOperationCopy;
if (isLocal) {
operation |= NSDragOperationMove;
}
if ([delegate_ canDragBookmarkButtonToTrash:self]) {
operation |= NSDragOperationDelete;
}
return operation;
}
- (void)draggedImage:(NSImage *)anImage
endedAt:(NSPoint)aPoint
operation:(NSDragOperation)operation {
gDraggedButton = nil;
// Inform delegate of drag source that we're finished dragging,
// so it can close auto-opened bookmark folders etc.
[delegate_ bookmarkDragDidEnd:self
operation:operation];
// Tell delegate if it should delete us.
if (operation & NSDragOperationDelete) {
dragEndScreenLocation_ = aPoint;
[delegate_ didDragBookmarkToTrash:self];
}
}
- (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
NSLeftMouseDraggedMask;
BOOL keepGoing = YES;
[[self target] performSelector:[self action] withObject:self];
self.draggableButton.actionHasFired = YES;
DraggableButton* insideBtn = nil;
while (keepGoing) {
theEvent = [[self window] nextEventMatchingMask:eventMask];
if (!theEvent)
continue;
NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
fromView:nil];
BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
switch ([theEvent type]) {
case NSMouseEntered:
case NSMouseExited: {
NSView* trackedView = (NSView*)[[theEvent trackingArea] owner];
if (trackedView && [trackedView isKindOfClass:[self class]]) {
BookmarkButton* btn = static_cast<BookmarkButton*>(trackedView);
if (![btn acceptsTrackInFrom:self])
break;
if ([theEvent type] == NSMouseEntered) {
[[NSCursor arrowCursor] set];
[[btn cell] mouseEntered:theEvent];
insideBtn = btn;
} else {
[[btn cell] mouseExited:theEvent];
if (insideBtn == btn)
insideBtn = nil;
}
}
break;
}
case NSLeftMouseDragged: {
if (insideBtn)
[insideBtn mouseDragged:theEvent];
break;
}
case NSLeftMouseUp: {
self.draggableButton.durationMouseWasDown =
[theEvent timestamp] - self.draggableButton.whenMouseDown;
if (!isInside && insideBtn && insideBtn != self) {
// Has tracked onto another BookmarkButton menu item, and released,
// so fire its action.
[[insideBtn target] performSelector:[insideBtn action]
withObject:insideBtn];
} else {
[self secondaryMouseUpAction:isInside];
[[self cell] mouseExited:theEvent];
[[insideBtn cell] mouseExited:theEvent];
}
keepGoing = NO;
break;
}
default:
/* Ignore any other kind of event. */
break;
}
}
return kDraggableButtonMixinDidWork;
}
// mouseEntered: and mouseExited: are called from our
// BookmarkButtonCell. We redirect this information to our delegate.
// The controller can then perform menu-like actions (e.g. "hover over
// to open menu").
- (void)mouseEntered:(NSEvent*)event {
[delegate_ mouseEnteredButton:self event:event];
}
// See comments above mouseEntered:.
- (void)mouseExited:(NSEvent*)event {
[delegate_ mouseExitedButton:self event:event];
}
- (void)mouseMoved:(NSEvent*)theEvent {
if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
[id(delegate_) mouseMoved:theEvent];
}
- (void)mouseDragged:(NSEvent*)theEvent {
if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
[id(delegate_) mouseDragged:theEvent];
}
- (void)rightMouseDown:(NSEvent*)event {
// Ensure that right-clicking on a button while a context menu is open
// highlights the new button.
GradientButtonCell* cell =
base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
[delegate_ mouseEnteredButton:self event:event];
[cell setMouseInside:YES animate:YES];
// Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
[super rightMouseDown:event];
if (![cell isMouseReallyInside]) {
[cell setMouseInside:NO animate:YES];
[delegate_ mouseExitedButton:self event:event];
}
}
+ (BookmarkButton*)draggedButton {
return gDraggedButton;
}
- (BOOL)canBecomeKeyView {
if (![super canBecomeKeyView])
return NO;
// If button is an item in a folder menu, don't become key.
return ![[self cell] isFolderButtonCell];
}
// This only gets called after a click that wasn't a drag, and only on folders.
- (DraggableButtonResult)secondaryMouseUpAction:(BOOL)wasInside {
const NSTimeInterval kShortClickLength = 0.5;
// Long clicks that end over the folder button result in the menu hiding.
if (wasInside &&
self.draggableButton.durationMouseWasDown > kShortClickLength) {
[[self target] performSelector:[self action] withObject:self];
} else {
// Mouse tracked out of button during menu track. Hide menus.
if (!wasInside)
[delegate_ bookmarkDragDidEnd:self
operation:NSDragOperationNone];
}
return kDraggableButtonMixinDidWork;
}
- (BOOL)isOpaque {
// Make this control opaque so that sub-pixel anti-aliasing works when
// CoreAnimation is enabled.
return YES;
}
- (void)drawRect:(NSRect)rect {
NSView* bookmarkBarToolbarView = [[self superview] superview];
[self cr_drawUsingAncestor:bookmarkBarToolbarView inRect:(NSRect)rect];
[super drawRect:rect];
}
@end
@implementation BookmarkButton(Private)
- (void)installCustomTrackingArea {
const NSTrackingAreaOptions options =
NSTrackingActiveAlways |
NSTrackingMouseEnteredAndExited |
NSTrackingEnabledDuringMouseDrag;
if (area_) {
[self removeTrackingArea:area_];
[area_ release];
}
area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:options
owner:self
userInfo:nil];
[self addTrackingArea:area_];
}
- (NSImage*)dragImage {
NSRect bounds = [self bounds];
base::scoped_nsobject<NSImage> image(
[[NSImage alloc] initWithSize:bounds.size]);
[image lockFocusFlipped:[self isFlipped]];
NSGraphicsContext* context = [NSGraphicsContext currentContext];
CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
CGContextBeginTransparencyLayer(cgContext, 0);
CGContextSetAlpha(cgContext, kDragImageOpacity);
GradientButtonCell* cell =
base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
[[cell clipPathForFrame:bounds inView:self] setClip];
[cell drawWithFrame:bounds inView:self];
CGContextEndTransparencyLayer(cgContext);
[image unlockFocus];
return image.autorelease();
}
@end // @implementation BookmarkButton(Private)