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 #include "chrome/browser/bookmarks/bookmark_model.h"
13 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
14 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
15 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
16 #import "chrome/browser/ui/cocoa/view_id_util.h"
17 #include "content/public/browser/user_metrics.h"
18 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
20 using base::UserMetricsAction;
22 // The opacity of the bookmark button drag image.
23 static const CGFloat kDragImageOpacity = 0.7;
26 namespace bookmark_button {
28 NSString* const kPulseBookmarkButtonNotification =
29 @"PulseBookmarkButtonNotification";
30 NSString* const kBookmarkKey = @"BookmarkKey";
31 NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
36 // We need a class variable to track the current dragged button to enable
37 // proper live animated dragging behavior, and can't do it in the
38 // delegate/controller since you can drag a button from one domain to the
39 // other (from a "folder" menu, to the main bar, or vice versa).
40 BookmarkButton* gDraggedButton = nil; // Weak
43 @interface BookmarkButton(Private)
45 // Make a drag image for the button.
46 - (NSImage*)dragImage;
48 - (void)installCustomTrackingArea;
50 @end // @interface BookmarkButton(Private)
53 @implementation BookmarkButton
55 @synthesize delegate = delegate_;
56 @synthesize acceptsTrackIn = acceptsTrackIn_;
58 - (id)initWithFrame:(NSRect)frameRect {
59 // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
60 // BookmarkBarController, so we can't just override -viewID method to return
62 if ((self = [super initWithFrame:frameRect])) {
63 view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
64 [self installCustomTrackingArea];
70 if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
71 [[self cell] safelyStopPulsing];
72 view_id_util::UnsetID(self);
75 [self removeTrackingArea:area_];
82 - (const BookmarkNode*)bookmarkNode {
83 return [[self cell] bookmarkNode];
87 const BookmarkNode* node = [self bookmarkNode];
88 return (node && node->is_folder());
92 return [self bookmarkNode] ? NO : YES;
95 - (void)setIsContinuousPulsing:(BOOL)flag {
96 [[self cell] setIsContinuousPulsing:flag];
99 - (BOOL)isContinuousPulsing {
100 return [[self cell] isContinuousPulsing];
103 - (NSPoint)screenLocationForRemoveAnimation {
107 // Use the position of the mouse in the drag image as the location.
108 point = dragEndScreenLocation_;
109 point.x += dragMouseOffset_.x;
110 if ([self isFlipped]) {
111 point.y += [self bounds].size.height - dragMouseOffset_.y;
113 point.y += dragMouseOffset_.y;
116 // Use the middle of this button as the location.
117 NSRect bounds = [self bounds];
118 point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
119 point = [self convertPoint:point toView:nil];
120 point = [[self window] convertBaseToScreen:point];
127 - (void)updateTrackingAreas {
128 [self installCustomTrackingArea];
129 [super updateTrackingAreas];
132 - (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
134 xHysteresis:(float)xHysteresis
135 yHysteresis:(float)yHysteresis
136 indicates:(BOOL*)result {
137 const float kDownProportion = 1.4142135f; // Square root of 2.
139 // We want to show a folder menu when you drag down on folder buttons,
140 // so don't classify this as a drag for that case.
141 if ([self isFolder] &&
142 (yDelta <= -yHysteresis) && // Bottom of hysteresis box was hit.
143 (std::abs(yDelta) / std::abs(xDelta)) >= kDownProportion) {
145 return kDraggableButtonMixinDidWork;
148 return kDraggableButtonImplUseBase;
152 // By default, NSButton ignores middle-clicks.
154 - (void)otherMouseUp:(NSEvent*)event {
155 [self performClick:self];
158 - (BOOL)acceptsTrackInFrom:(id)sender {
159 return [self isFolder] || [self acceptsTrackIn];
163 // Overridden from DraggableButton.
164 - (void)beginDrag:(NSEvent*)event {
165 // Don't allow a drag of the empty node.
166 // The empty node is a placeholder for "(empty)", to be revisited.
170 if (![self delegate]) {
175 if ([self isFolder]) {
176 // Close the folder's drop-down menu if it's visible.
177 [[self target] closeBookmarkFolder:self];
180 // At the moment, moving bookmarks causes their buttons (like me!)
181 // to be destroyed and rebuilt. Make sure we don't go away while on
185 // Ask our delegate to fill the pasteboard for us.
186 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
187 [[self delegate] fillPasteboard:pboard forDragOfButton:self];
189 // Lock bar visibility, forcing the overlay to stay visible if we are in
191 if ([[self delegate] dragShouldLockBarVisibility]) {
192 DCHECK(!visibilityDelegate_);
193 NSWindow* window = [[self delegate] browserWindow];
194 visibilityDelegate_ =
195 [BrowserWindowController browserWindowControllerForWindow:window];
196 [visibilityDelegate_ lockBarVisibilityForOwner:self
200 const BookmarkNode* node = [self bookmarkNode];
201 const BookmarkNode* parent = node ? node->parent() : NULL;
202 if (parent && parent->type() == BookmarkNode::FOLDER) {
203 content::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
205 content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
208 dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
210 gDraggedButton = self;
212 CGFloat yAt = [self bounds].size.height;
213 NSSize dragOffset = NSMakeSize(0.0, 0.0);
214 NSImage* image = [self dragImage];
215 [self setHidden:YES];
216 [self dragImage:image at:NSMakePoint(0, yAt) offset:dragOffset
217 event:event pasteboard:pboard source:self slideBack:YES];
222 gDraggedButton = nil;
227 // Overridden to release bar visibility.
228 - (DraggableButtonResult)endDrag {
229 gDraggedButton = nil;
231 // visibilityDelegate_ can be nil if we're detached, and that's fine.
232 [visibilityDelegate_ releaseBarVisibilityForOwner:self
235 visibilityDelegate_ = nil;
237 return kDraggableButtonImplUseBase;
240 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
241 NSDragOperation operation = NSDragOperationCopy;
243 operation |= NSDragOperationMove;
245 if ([delegate_ canDragBookmarkButtonToTrash:self]) {
246 operation |= NSDragOperationDelete;
251 - (void)draggedImage:(NSImage *)anImage
252 endedAt:(NSPoint)aPoint
253 operation:(NSDragOperation)operation {
254 gDraggedButton = nil;
255 // Inform delegate of drag source that we're finished dragging,
256 // so it can close auto-opened bookmark folders etc.
257 [delegate_ bookmarkDragDidEnd:self
258 operation:operation];
259 // Tell delegate if it should delete us.
260 if (operation & NSDragOperationDelete) {
261 dragEndScreenLocation_ = aPoint;
262 [delegate_ didDragBookmarkToTrash:self];
266 - (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
267 int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
268 NSLeftMouseDraggedMask;
270 BOOL keepGoing = YES;
271 [[self target] performSelector:[self action] withObject:self];
272 self.draggableButton.actionHasFired = YES;
274 DraggableButton* insideBtn = nil;
277 theEvent = [[self window] nextEventMatchingMask:eventMask];
281 NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
283 BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
285 switch ([theEvent type]) {
287 case NSMouseExited: {
288 NSView* trackedView = (NSView*)[[theEvent trackingArea] owner];
289 if (trackedView && [trackedView isKindOfClass:[self class]]) {
290 BookmarkButton* btn = static_cast<BookmarkButton*>(trackedView);
291 if (![btn acceptsTrackInFrom:self])
293 if ([theEvent type] == NSMouseEntered) {
294 [[NSCursor arrowCursor] set];
295 [[btn cell] mouseEntered:theEvent];
298 [[btn cell] mouseExited:theEvent];
299 if (insideBtn == btn)
305 case NSLeftMouseDragged: {
307 [insideBtn mouseDragged:theEvent];
310 case NSLeftMouseUp: {
311 self.draggableButton.durationMouseWasDown =
312 [theEvent timestamp] - self.draggableButton.whenMouseDown;
313 if (!isInside && insideBtn && insideBtn != self) {
314 // Has tracked onto another BookmarkButton menu item, and released,
315 // so fire its action.
316 [[insideBtn target] performSelector:[insideBtn action]
317 withObject:insideBtn];
320 [self secondaryMouseUpAction:isInside];
321 [[self cell] mouseExited:theEvent];
322 [[insideBtn cell] mouseExited:theEvent];
328 /* Ignore any other kind of event. */
332 return kDraggableButtonMixinDidWork;
337 // mouseEntered: and mouseExited: are called from our
338 // BookmarkButtonCell. We redirect this information to our delegate.
339 // The controller can then perform menu-like actions (e.g. "hover over
341 - (void)mouseEntered:(NSEvent*)event {
342 [delegate_ mouseEnteredButton:self event:event];
345 // See comments above mouseEntered:.
346 - (void)mouseExited:(NSEvent*)event {
347 [delegate_ mouseExitedButton:self event:event];
350 - (void)mouseMoved:(NSEvent*)theEvent {
351 if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
352 [id(delegate_) mouseMoved:theEvent];
355 - (void)mouseDragged:(NSEvent*)theEvent {
356 if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
357 [id(delegate_) mouseDragged:theEvent];
360 - (void)rightMouseDown:(NSEvent*)event {
361 // Ensure that right-clicking on a button while a context menu is open
362 // highlights the new button.
363 GradientButtonCell* cell =
364 base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
365 [delegate_ mouseEnteredButton:self event:event];
366 [cell setMouseInside:YES animate:YES];
368 // Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
369 base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
370 [super rightMouseDown:event];
372 if (![cell isMouseReallyInside]) {
373 [cell setMouseInside:NO animate:YES];
374 [delegate_ mouseExitedButton:self event:event];
378 + (BookmarkButton*)draggedButton {
379 return gDraggedButton;
382 - (BOOL)canBecomeKeyView {
383 if (![super canBecomeKeyView])
386 // If button is an item in a folder menu, don't become key.
387 return ![[self cell] isFolderButtonCell];
390 // This only gets called after a click that wasn't a drag, and only on folders.
391 - (DraggableButtonResult)secondaryMouseUpAction:(BOOL)wasInside {
392 const NSTimeInterval kShortClickLength = 0.5;
393 // Long clicks that end over the folder button result in the menu hiding.
395 self.draggableButton.durationMouseWasDown > kShortClickLength) {
396 [[self target] performSelector:[self action] withObject:self];
398 // Mouse tracked out of button during menu track. Hide menus.
400 [delegate_ bookmarkDragDidEnd:self
401 operation:NSDragOperationNone];
403 return kDraggableButtonMixinDidWork;
407 // Make this control opaque so that sub pixel anti aliasing works when core
408 // animation is enabled.
412 - (void)drawRect:(NSRect)rect {
413 // Draw the toolbar background.
415 gfx::ScopedNSGraphicsContextSaveGState scopedGSState;
416 NSView* toolbarView = [[self superview] superview];
417 NSRect frame = [self convertRect:[self bounds] toView:toolbarView];
419 NSAffineTransform* transform = [NSAffineTransform transform];
420 [transform translateXBy:-NSMinX(frame) yBy:-NSMinY(frame)];
423 [toolbarView drawRect:[toolbarView bounds]];
426 [super drawRect:rect];
431 @implementation BookmarkButton(Private)
434 - (void)installCustomTrackingArea {
435 const NSTrackingAreaOptions options =
436 NSTrackingActiveAlways |
437 NSTrackingMouseEnteredAndExited |
438 NSTrackingEnabledDuringMouseDrag;
441 [self removeTrackingArea:area_];
445 area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
449 [self addTrackingArea:area_];
453 - (NSImage*)dragImage {
454 NSRect bounds = [self bounds];
455 base::scoped_nsobject<NSImage> image(
456 [[NSImage alloc] initWithSize:bounds.size]);
457 [image lockFocusFlipped:[self isFlipped]];
459 NSGraphicsContext* context = [NSGraphicsContext currentContext];
460 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
461 CGContextBeginTransparencyLayer(cgContext, 0);
462 CGContextSetAlpha(cgContext, kDragImageOpacity);
464 GradientButtonCell* cell =
465 base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
466 [[cell clipPathForFrame:bounds inView:self] setClip];
467 [cell drawWithFrame:bounds inView:self];
469 CGContextEndTransparencyLayer(cgContext);
472 return image.autorelease();
475 @end // @implementation BookmarkButton(Private)