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