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;
28 // We need a class variable to track the current dragged button to enable
29 // proper live animated dragging behavior, and can't do it in the
30 // delegate/controller since you can drag a button from one domain to the
31 // other (from a "folder" menu, to the main bar, or vice versa).
32 BookmarkButton* gDraggedButton = nil; // Weak
35 @interface BookmarkButton(Private)
37 // Make a drag image for the button.
38 - (NSImage*)dragImage;
40 - (void)installCustomTrackingArea;
42 @end // @interface BookmarkButton(Private)
45 @implementation BookmarkButton
47 @synthesize delegate = delegate_;
48 @synthesize acceptsTrackIn = acceptsTrackIn_;
50 - (id)initWithFrame:(NSRect)frameRect {
51 // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
52 // BookmarkBarController, so we can't just override -viewID method to return
54 if ((self = [super initWithFrame:frameRect])) {
55 view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
56 [self installCustomTrackingArea];
62 if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
63 [[self cell] safelyStopPulsing];
64 view_id_util::UnsetID(self);
67 [self removeTrackingArea:area_];
74 - (const BookmarkNode*)bookmarkNode {
75 return [[self cell] bookmarkNode];
79 const BookmarkNode* node = [self bookmarkNode];
80 return (node && node->is_folder());
84 return [self bookmarkNode] ? NO : YES;
87 - (void)setIsContinuousPulsing:(BOOL)flag {
88 [[self cell] setIsContinuousPulsing:flag];
91 - (BOOL)isContinuousPulsing {
92 return [[self cell] isContinuousPulsing];
95 - (NSPoint)screenLocationForRemoveAnimation {
99 // Use the position of the mouse in the drag image as the location.
100 point = dragEndScreenLocation_;
101 point.x += dragMouseOffset_.x;
102 if ([self isFlipped]) {
103 point.y += [self bounds].size.height - dragMouseOffset_.y;
105 point.y += dragMouseOffset_.y;
108 // Use the middle of this button as the location.
109 NSRect bounds = [self bounds];
110 point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
111 point = [self convertPoint:point toView:nil];
112 point = [[self window] convertBaseToScreen:point];
119 - (void)updateTrackingAreas {
120 [self installCustomTrackingArea];
121 [super updateTrackingAreas];
124 - (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
126 xHysteresis:(float)xHysteresis
127 yHysteresis:(float)yHysteresis
128 indicates:(BOOL*)result {
129 const float kDownProportion = 1.4142135f; // Square root of 2.
131 // We want to show a folder menu when you drag down on folder buttons,
132 // so don't classify this as a drag for that case.
133 if ([self isFolder] &&
134 (yDelta <= -yHysteresis) && // Bottom of hysteresis box was hit.
135 (std::abs(yDelta) / std::abs(xDelta)) >= kDownProportion) {
137 return kDraggableButtonMixinDidWork;
140 return kDraggableButtonImplUseBase;
144 // By default, NSButton ignores middle-clicks.
146 - (void)otherMouseUp:(NSEvent*)event {
147 [self performClick:self];
150 - (BOOL)acceptsTrackInFrom:(id)sender {
151 return [self isFolder] || [self acceptsTrackIn];
155 // Overridden from DraggableButton.
156 - (void)beginDrag:(NSEvent*)event {
157 // Don't allow a drag of the empty node.
158 // The empty node is a placeholder for "(empty)", to be revisited.
162 if (![self delegate]) {
167 if ([self isFolder]) {
168 // Close the folder's drop-down menu if it's visible.
169 [[self target] closeBookmarkFolder:self];
172 // At the moment, moving bookmarks causes their buttons (like me!)
173 // to be destroyed and rebuilt. Make sure we don't go away while on
177 // Ask our delegate to fill the pasteboard for us.
178 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
179 [[self delegate] fillPasteboard:pboard forDragOfButton:self];
181 // Lock bar visibility, forcing the overlay to stay visible if we are in
183 if ([[self delegate] dragShouldLockBarVisibility]) {
184 DCHECK(!visibilityDelegate_);
185 NSWindow* window = [[self delegate] browserWindow];
186 visibilityDelegate_ =
187 [BrowserWindowController browserWindowControllerForWindow:window];
188 [visibilityDelegate_ lockBarVisibilityForOwner:self
192 const BookmarkNode* node = [self bookmarkNode];
193 const BookmarkNode* parent = node ? node->parent() : NULL;
194 if (parent && parent->type() == BookmarkNode::FOLDER) {
195 content::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
197 content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
200 dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
202 gDraggedButton = self;
204 CGFloat yAt = [self bounds].size.height;
205 NSSize dragOffset = NSMakeSize(0.0, 0.0);
206 NSImage* image = [self dragImage];
207 [self setHidden:YES];
208 [self dragImage:image at:NSMakePoint(0, yAt) offset:dragOffset
209 event:event pasteboard:pboard source:self slideBack:YES];
214 gDraggedButton = nil;
219 // Overridden to release bar visibility.
220 - (DraggableButtonResult)endDrag {
221 gDraggedButton = nil;
223 // visibilityDelegate_ can be nil if we're detached, and that's fine.
224 [visibilityDelegate_ releaseBarVisibilityForOwner:self
227 visibilityDelegate_ = nil;
229 return kDraggableButtonImplUseBase;
232 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
233 NSDragOperation operation = NSDragOperationCopy;
235 operation |= NSDragOperationMove;
237 if ([delegate_ canDragBookmarkButtonToTrash:self]) {
238 operation |= NSDragOperationDelete;
243 - (void)draggedImage:(NSImage *)anImage
244 endedAt:(NSPoint)aPoint
245 operation:(NSDragOperation)operation {
246 gDraggedButton = nil;
247 // Inform delegate of drag source that we're finished dragging,
248 // so it can close auto-opened bookmark folders etc.
249 [delegate_ bookmarkDragDidEnd:self
250 operation:operation];
251 // Tell delegate if it should delete us.
252 if (operation & NSDragOperationDelete) {
253 dragEndScreenLocation_ = aPoint;
254 [delegate_ didDragBookmarkToTrash:self];
258 - (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
259 int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
260 NSLeftMouseDraggedMask;
262 BOOL keepGoing = YES;
263 [[self target] performSelector:[self action] withObject:self];
264 self.draggableButton.actionHasFired = YES;
266 DraggableButton* insideBtn = nil;
269 theEvent = [[self window] nextEventMatchingMask:eventMask];
273 NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
275 BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
277 switch ([theEvent type]) {
279 case NSMouseExited: {
280 NSView* trackedView = (NSView*)[[theEvent trackingArea] owner];
281 if (trackedView && [trackedView isKindOfClass:[self class]]) {
282 BookmarkButton* btn = static_cast<BookmarkButton*>(trackedView);
283 if (![btn acceptsTrackInFrom:self])
285 if ([theEvent type] == NSMouseEntered) {
286 [[NSCursor arrowCursor] set];
287 [[btn cell] mouseEntered:theEvent];
290 [[btn cell] mouseExited:theEvent];
291 if (insideBtn == btn)
297 case NSLeftMouseDragged: {
299 [insideBtn mouseDragged:theEvent];
302 case NSLeftMouseUp: {
303 self.draggableButton.durationMouseWasDown =
304 [theEvent timestamp] - self.draggableButton.whenMouseDown;
305 if (!isInside && insideBtn && insideBtn != self) {
306 // Has tracked onto another BookmarkButton menu item, and released,
307 // so fire its action.
308 [[insideBtn target] performSelector:[insideBtn action]
309 withObject:insideBtn];
312 [self secondaryMouseUpAction:isInside];
313 [[self cell] mouseExited:theEvent];
314 [[insideBtn cell] mouseExited:theEvent];
320 /* Ignore any other kind of event. */
324 return kDraggableButtonMixinDidWork;
329 // mouseEntered: and mouseExited: are called from our
330 // BookmarkButtonCell. We redirect this information to our delegate.
331 // The controller can then perform menu-like actions (e.g. "hover over
333 - (void)mouseEntered:(NSEvent*)event {
334 [delegate_ mouseEnteredButton:self event:event];
337 // See comments above mouseEntered:.
338 - (void)mouseExited:(NSEvent*)event {
339 [delegate_ mouseExitedButton:self event:event];
342 - (void)mouseMoved:(NSEvent*)theEvent {
343 if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
344 [id(delegate_) mouseMoved:theEvent];
347 - (void)mouseDragged:(NSEvent*)theEvent {
348 if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
349 [id(delegate_) mouseDragged:theEvent];
352 - (void)rightMouseDown:(NSEvent*)event {
353 // Ensure that right-clicking on a button while a context menu is open
354 // highlights the new button.
355 GradientButtonCell* cell =
356 base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
357 [delegate_ mouseEnteredButton:self event:event];
358 [cell setMouseInside:YES animate:YES];
360 // Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
361 base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
362 [super rightMouseDown:event];
364 if (![cell isMouseReallyInside]) {
365 [cell setMouseInside:NO animate:YES];
366 [delegate_ mouseExitedButton:self event:event];
370 + (BookmarkButton*)draggedButton {
371 return gDraggedButton;
374 - (BOOL)canBecomeKeyView {
375 if (![super canBecomeKeyView])
378 // If button is an item in a folder menu, don't become key.
379 return ![[self cell] isFolderButtonCell];
382 // This only gets called after a click that wasn't a drag, and only on folders.
383 - (DraggableButtonResult)secondaryMouseUpAction:(BOOL)wasInside {
384 const NSTimeInterval kShortClickLength = 0.5;
385 // Long clicks that end over the folder button result in the menu hiding.
387 self.draggableButton.durationMouseWasDown > kShortClickLength) {
388 [[self target] performSelector:[self action] withObject:self];
390 // Mouse tracked out of button during menu track. Hide menus.
392 [delegate_ bookmarkDragDidEnd:self
393 operation:NSDragOperationNone];
395 return kDraggableButtonMixinDidWork;
399 // Make this control opaque so that sub-pixel anti-aliasing works when
400 // CoreAnimation is enabled.
404 - (void)drawRect:(NSRect)rect {
405 NSView* bookmarkBarToolbarView = [[self superview] superview];
406 [self cr_drawUsingAncestor:bookmarkBarToolbarView inRect:(NSRect)rect];
407 [super drawRect:rect];
410 - (void)viewDidMoveToWindow {
411 [super viewDidMoveToWindow];
413 // The new window may have different main window status.
414 // This happens when the view is moved into a TabWindowOverlayWindow for
416 [self windowDidChangeActive];
420 // ThemedWindowDrawing implementation.
422 - (void)windowDidChangeTheme {
423 [self setNeedsDisplay:YES];
426 - (void)windowDidChangeActive {
427 [self setNeedsDisplay:YES];
432 @implementation BookmarkButton(Private)
435 - (void)installCustomTrackingArea {
436 const NSTrackingAreaOptions options =
437 NSTrackingActiveAlways |
438 NSTrackingMouseEnteredAndExited |
439 NSTrackingEnabledDuringMouseDrag;
442 [self removeTrackingArea:area_];
446 area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
450 [self addTrackingArea:area_];
454 - (NSImage*)dragImage {
455 NSRect bounds = [self bounds];
456 base::scoped_nsobject<NSImage> image(
457 [[NSImage alloc] initWithSize:bounds.size]);
458 [image lockFocusFlipped:[self isFlipped]];
460 NSGraphicsContext* context = [NSGraphicsContext currentContext];
461 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
462 CGContextBeginTransparencyLayer(cgContext, 0);
463 CGContextSetAlpha(cgContext, kDragImageOpacity);
465 GradientButtonCell* cell =
466 base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
467 [[cell clipPathForFrame:bounds inView:self] setClip];
468 [cell drawWithFrame:bounds inView:self];
470 CGContextEndTransparencyLayer(cgContext);
473 return image.autorelease();
476 @end // @implementation BookmarkButton(Private)