1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
9 #include "base/logging.h"
10 #include "base/mac/foundation_util.h"
11 #import "base/mac/scoped_nsobject.h"
12 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
13 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
14 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
15 #import "chrome/browser/ui/cocoa/view_id_util.h"
16 #include "components/bookmarks/browser/bookmark_model.h"
17 #include "content/public/browser/user_metrics.h"
18 #import "ui/base/cocoa/nsview_additions.h"
19 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
21 using base::UserMetricsAction;
22 using bookmarks::BookmarkNode;
24 // The opacity of the bookmark button drag image.
25 static const CGFloat kDragImageOpacity = 0.7;
27 namespace bookmark_button {
29 NSString* const kPulseBookmarkButtonNotification =
30 @"PulseBookmarkButtonNotification";
31 NSString* const kBookmarkKey = @"BookmarkKey";
32 NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
37 // We need a class variable to track the current dragged button to enable
38 // proper live animated dragging behavior, and can't do it in the
39 // delegate/controller since you can drag a button from one domain to the
40 // other (from a "folder" menu, to the main bar, or vice versa).
41 BookmarkButton* gDraggedButton = nil; // Weak
44 @interface BookmarkButton(Private)
46 // Make a drag image for the button.
47 - (NSImage*)dragImage;
49 - (void)installCustomTrackingArea;
51 @end // @interface BookmarkButton(Private)
54 @implementation BookmarkButton
56 @synthesize delegate = delegate_;
57 @synthesize acceptsTrackIn = acceptsTrackIn_;
59 - (id)initWithFrame:(NSRect)frameRect {
60 // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
61 // BookmarkBarController, so we can't just override -viewID method to return
63 if ((self = [super initWithFrame:frameRect])) {
64 view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
65 [self installCustomTrackingArea];
71 if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
72 [[self cell] safelyStopPulsing];
73 view_id_util::UnsetID(self);
76 [self removeTrackingArea:area_];
83 - (const BookmarkNode*)bookmarkNode {
84 return [[self cell] bookmarkNode];
88 const BookmarkNode* node = [self bookmarkNode];
89 return (node && node->is_folder());
93 return [self bookmarkNode] ? NO : YES;
96 - (void)setIsContinuousPulsing:(BOOL)flag {
97 [[self cell] setIsContinuousPulsing:flag];
100 - (BOOL)isContinuousPulsing {
101 return [[self cell] isContinuousPulsing];
104 - (NSPoint)screenLocationForRemoveAnimation {
108 // Use the position of the mouse in the drag image as the location.
109 point = dragEndScreenLocation_;
110 point.x += dragMouseOffset_.x;
111 if ([self isFlipped]) {
112 point.y += [self bounds].size.height - dragMouseOffset_.y;
114 point.y += dragMouseOffset_.y;
117 // Use the middle of this button as the location.
118 NSRect bounds = [self bounds];
119 point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
120 point = [self convertPoint:point toView:nil];
121 point = [[self window] convertBaseToScreen:point];
128 - (void)updateTrackingAreas {
129 [self installCustomTrackingArea];
130 [super updateTrackingAreas];
133 - (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
135 xHysteresis:(float)xHysteresis
136 yHysteresis:(float)yHysteresis
137 indicates:(BOOL*)result {
138 const float kDownProportion = 1.4142135f; // Square root of 2.
140 // We want to show a folder menu when you drag down on folder buttons,
141 // so don't classify this as a drag for that case.
142 if ([self isFolder] &&
143 (yDelta <= -yHysteresis) && // Bottom of hysteresis box was hit.
144 (std::abs(yDelta) / std::abs(xDelta)) >= kDownProportion) {
146 return kDraggableButtonMixinDidWork;
149 return kDraggableButtonImplUseBase;
153 // By default, NSButton ignores middle-clicks.
155 - (void)otherMouseUp:(NSEvent*)event {
156 [self performClick:self];
159 - (BOOL)acceptsTrackInFrom:(id)sender {
160 return [self isFolder] || [self acceptsTrackIn];
164 // Overridden from DraggableButton.
165 - (void)beginDrag:(NSEvent*)event {
166 // Don't allow a drag of the empty node.
167 // The empty node is a placeholder for "(empty)", to be revisited.
171 if (![self delegate]) {
176 if ([self isFolder]) {
177 // Close the folder's drop-down menu if it's visible.
178 [[self target] closeBookmarkFolder:self];
181 // At the moment, moving bookmarks causes their buttons (like me!)
182 // to be destroyed and rebuilt. Make sure we don't go away while on
186 // Ask our delegate to fill the pasteboard for us.
187 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
188 [[self delegate] fillPasteboard:pboard forDragOfButton:self];
190 // Lock bar visibility, forcing the overlay to stay visible if we are in
192 if ([[self delegate] dragShouldLockBarVisibility]) {
193 DCHECK(!visibilityDelegate_);
194 NSWindow* window = [[self delegate] browserWindow];
195 visibilityDelegate_ =
196 [BrowserWindowController browserWindowControllerForWindow:window];
197 [visibilityDelegate_ lockBarVisibilityForOwner:self
201 const BookmarkNode* node = [self bookmarkNode];
202 const BookmarkNode* parent = node ? node->parent() : NULL;
203 if (parent && parent->type() == BookmarkNode::FOLDER) {
204 content::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
206 content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
209 dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
211 gDraggedButton = self;
213 CGFloat yAt = [self bounds].size.height;
214 NSSize dragOffset = NSMakeSize(0.0, 0.0);
215 NSImage* image = [self dragImage];
216 [self setHidden:YES];
217 [self dragImage:image at:NSMakePoint(0, yAt) offset:dragOffset
218 event:event pasteboard:pboard source:self slideBack:YES];
223 gDraggedButton = nil;
228 // Overridden to release bar visibility.
229 - (DraggableButtonResult)endDrag {
230 gDraggedButton = nil;
232 // visibilityDelegate_ can be nil if we're detached, and that's fine.
233 [visibilityDelegate_ releaseBarVisibilityForOwner:self
236 visibilityDelegate_ = nil;
238 return kDraggableButtonImplUseBase;
241 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
242 NSDragOperation operation = NSDragOperationCopy;
244 operation |= NSDragOperationMove;
246 if ([delegate_ canDragBookmarkButtonToTrash:self]) {
247 operation |= NSDragOperationDelete;
252 - (void)draggedImage:(NSImage *)anImage
253 endedAt:(NSPoint)aPoint
254 operation:(NSDragOperation)operation {
255 gDraggedButton = nil;
256 // Inform delegate of drag source that we're finished dragging,
257 // so it can close auto-opened bookmark folders etc.
258 [delegate_ bookmarkDragDidEnd:self
259 operation:operation];
260 // Tell delegate if it should delete us.
261 if (operation & NSDragOperationDelete) {
262 dragEndScreenLocation_ = aPoint;
263 [delegate_ didDragBookmarkToTrash:self];
267 - (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
268 int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
269 NSLeftMouseDraggedMask;
271 BOOL keepGoing = YES;
272 [[self target] performSelector:[self action] withObject:self];
273 self.draggableButton.actionHasFired = YES;
275 DraggableButton* insideBtn = nil;
278 theEvent = [[self window] nextEventMatchingMask:eventMask];
282 NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
284 BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
286 switch ([theEvent type]) {
288 case NSMouseExited: {
289 NSView* trackedView = (NSView*)[[theEvent trackingArea] owner];
290 if (trackedView && [trackedView isKindOfClass:[self class]]) {
291 BookmarkButton* btn = static_cast<BookmarkButton*>(trackedView);
292 if (![btn acceptsTrackInFrom:self])
294 if ([theEvent type] == NSMouseEntered) {
295 [[NSCursor arrowCursor] set];
296 [[btn cell] mouseEntered:theEvent];
299 [[btn cell] mouseExited:theEvent];
300 if (insideBtn == btn)
306 case NSLeftMouseDragged: {
308 [insideBtn mouseDragged:theEvent];
311 case NSLeftMouseUp: {
312 self.draggableButton.durationMouseWasDown =
313 [theEvent timestamp] - self.draggableButton.whenMouseDown;
314 if (!isInside && insideBtn && insideBtn != self) {
315 // Has tracked onto another BookmarkButton menu item, and released,
316 // so fire its action.
317 [[insideBtn target] performSelector:[insideBtn action]
318 withObject:insideBtn];
321 [self secondaryMouseUpAction:isInside];
322 [[self cell] mouseExited:theEvent];
323 [[insideBtn cell] mouseExited:theEvent];
329 /* Ignore any other kind of event. */
333 return kDraggableButtonMixinDidWork;
338 // mouseEntered: and mouseExited: are called from our
339 // BookmarkButtonCell. We redirect this information to our delegate.
340 // The controller can then perform menu-like actions (e.g. "hover over
342 - (void)mouseEntered:(NSEvent*)event {
343 [delegate_ mouseEnteredButton:self event:event];
346 // See comments above mouseEntered:.
347 - (void)mouseExited:(NSEvent*)event {
348 [delegate_ mouseExitedButton:self event:event];
351 - (void)mouseMoved:(NSEvent*)theEvent {
352 if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
353 [id(delegate_) mouseMoved:theEvent];
356 - (void)mouseDragged:(NSEvent*)theEvent {
357 if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
358 [id(delegate_) mouseDragged:theEvent];
361 - (void)rightMouseDown:(NSEvent*)event {
362 // Ensure that right-clicking on a button while a context menu is open
363 // highlights the new button.
364 GradientButtonCell* cell =
365 base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
366 [delegate_ mouseEnteredButton:self event:event];
367 [cell setMouseInside:YES animate:YES];
369 // Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
370 base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
371 [super rightMouseDown:event];
373 if (![cell isMouseReallyInside]) {
374 [cell setMouseInside:NO animate:YES];
375 [delegate_ mouseExitedButton:self event:event];
379 + (BookmarkButton*)draggedButton {
380 return gDraggedButton;
383 - (BOOL)canBecomeKeyView {
384 if (![super canBecomeKeyView])
387 // If button is an item in a folder menu, don't become key.
388 return ![[self cell] isFolderButtonCell];
391 // This only gets called after a click that wasn't a drag, and only on folders.
392 - (DraggableButtonResult)secondaryMouseUpAction:(BOOL)wasInside {
393 const NSTimeInterval kShortClickLength = 0.5;
394 // Long clicks that end over the folder button result in the menu hiding.
396 self.draggableButton.durationMouseWasDown > kShortClickLength) {
397 [[self target] performSelector:[self action] withObject:self];
399 // Mouse tracked out of button during menu track. Hide menus.
401 [delegate_ bookmarkDragDidEnd:self
402 operation:NSDragOperationNone];
404 return kDraggableButtonMixinDidWork;
408 // Make this control opaque so that sub-pixel anti-aliasing works when
409 // CoreAnimation is enabled.
413 - (void)drawRect:(NSRect)rect {
414 NSView* bookmarkBarToolbarView = [[self superview] superview];
415 [self cr_drawUsingAncestor:bookmarkBarToolbarView inRect:(NSRect)rect];
416 [super drawRect:rect];
419 - (void)viewDidMoveToWindow {
420 [super viewDidMoveToWindow];
422 // The new window may have different main window status.
423 // This happens when the view is moved into a TabWindowOverlayWindow for
425 [self windowDidChangeActive];
429 // ThemedWindowDrawing implementation.
431 - (void)windowDidChangeTheme {
432 [self setNeedsDisplay:YES];
435 - (void)windowDidChangeActive {
436 [self setNeedsDisplay:YES];
441 @implementation BookmarkButton(Private)
444 - (void)installCustomTrackingArea {
445 const NSTrackingAreaOptions options =
446 NSTrackingActiveAlways |
447 NSTrackingMouseEnteredAndExited |
448 NSTrackingEnabledDuringMouseDrag;
451 [self removeTrackingArea:area_];
455 area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
459 [self addTrackingArea:area_];
463 - (NSImage*)dragImage {
464 NSRect bounds = [self bounds];
465 base::scoped_nsobject<NSImage> image(
466 [[NSImage alloc] initWithSize:bounds.size]);
467 [image lockFocusFlipped:[self isFlipped]];
469 NSGraphicsContext* context = [NSGraphicsContext currentContext];
470 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
471 CGContextBeginTransparencyLayer(cgContext, 0);
472 CGContextSetAlpha(cgContext, kDragImageOpacity);
474 GradientButtonCell* cell =
475 base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
476 [[cell clipPathForFrame:bounds inView:self] setClip];
477 [cell drawWithFrame:bounds inView:self];
479 CGContextEndTransparencyLayer(cgContext);
482 return image.autorelease();
485 @end // @implementation BookmarkButton(Private)