Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / bookmarks / bookmark_button.mm
blobbb689e5d4f5d82c47e4a17dac17b922fa5641bf4
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"
7 #include <cmath>
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 {
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
53   // it.
54   if ((self = [super initWithFrame:frameRect])) {
55     view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
56     [self installCustomTrackingArea];
57   }
58   return self;
61 - (void)dealloc {
62   if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
63     [[self cell] safelyStopPulsing];
64   view_id_util::UnsetID(self);
66   if (area_) {
67     [self removeTrackingArea:area_];
68     [area_ release];
69   }
71   [super dealloc];
74 - (const BookmarkNode*)bookmarkNode {
75   return [[self cell] bookmarkNode];
78 - (BOOL)isFolder {
79   const BookmarkNode* node = [self bookmarkNode];
80   return (node && node->is_folder());
83 - (BOOL)isEmpty {
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 {
96   NSPoint point;
98   if (dragPending_) {
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;
104     } else {
105       point.y += dragMouseOffset_.y;
106     }
107   } else {
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];
113   }
115   return point;
119 - (void)updateTrackingAreas {
120   [self installCustomTrackingArea];
121   [super updateTrackingAreas];
124 - (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
125                                                     yDelta:(float)yDelta
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) {
136     *result = NO;
137     return kDraggableButtonMixinDidWork;
138   }
140   return kDraggableButtonImplUseBase;
144 // By default, NSButton ignores middle-clicks.
145 // But we want them.
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.
159   if ([self isEmpty])
160     return;
162   if (![self delegate]) {
163     NOTREACHED();
164     return;
165   }
167   if ([self isFolder]) {
168     // Close the folder's drop-down menu if it's visible.
169     [[self target] closeBookmarkFolder:self];
170   }
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
174   // the stack.
175   [self retain];
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
182   // fullscreen mode.
183   if ([[self delegate] dragShouldLockBarVisibility]) {
184     DCHECK(!visibilityDelegate_);
185     NSWindow* window = [[self delegate] browserWindow];
186     visibilityDelegate_ =
187         [BrowserWindowController browserWindowControllerForWindow:window];
188     [visibilityDelegate_ lockBarVisibilityForOwner:self
189                                      withAnimation:NO
190                                              delay:NO];
191   }
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"));
196   } else {
197     content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
198   }
200   dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
201   dragPending_ = YES;
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];
210   [self setHidden:NO];
212   // And we're done.
213   dragPending_ = NO;
214   gDraggedButton = nil;
216   [self autorelease];
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
225                                       withAnimation:YES
226                                               delay:YES];
227   visibilityDelegate_ = nil;
229   return kDraggableButtonImplUseBase;
232 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
233   NSDragOperation operation = NSDragOperationCopy;
234   if (isLocal) {
235     operation |= NSDragOperationMove;
236   }
237   if ([delegate_ canDragBookmarkButtonToTrash:self]) {
238     operation |= NSDragOperationDelete;
239   }
240   return operation;
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];
255   }
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;
268   while (keepGoing) {
269     theEvent = [[self window] nextEventMatchingMask:eventMask];
270     if (!theEvent)
271       continue;
273     NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
274                                  fromView:nil];
275     BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
277     switch ([theEvent type]) {
278       case NSMouseEntered:
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])
284             break;
285           if ([theEvent type] == NSMouseEntered) {
286             [[NSCursor arrowCursor] set];
287             [[btn cell] mouseEntered:theEvent];
288             insideBtn = btn;
289           } else {
290             [[btn cell] mouseExited:theEvent];
291             if (insideBtn == btn)
292               insideBtn = nil;
293           }
294         }
295         break;
296       }
297       case NSLeftMouseDragged: {
298         if (insideBtn)
299           [insideBtn mouseDragged:theEvent];
300         break;
301       }
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];
311         } else {
312           [self secondaryMouseUpAction:isInside];
313           [[self cell] mouseExited:theEvent];
314           [[insideBtn cell] mouseExited:theEvent];
315         }
316         keepGoing = NO;
317         break;
318       }
319       default:
320         /* Ignore any other kind of event. */
321         break;
322     }
323   }
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
332 // to open menu").
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];
367   }
370 + (BookmarkButton*)draggedButton {
371   return gDraggedButton;
374 - (BOOL)canBecomeKeyView {
375   if (![super canBecomeKeyView])
376     return NO;
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.
386   if (wasInside &&
387       self.draggableButton.durationMouseWasDown > kShortClickLength) {
388     [[self target] performSelector:[self action] withObject:self];
389   } else {
390     // Mouse tracked out of button during menu track. Hide menus.
391     if (!wasInside)
392       [delegate_ bookmarkDragDidEnd:self
393                           operation:NSDragOperationNone];
394   }
395   return kDraggableButtonMixinDidWork;
398 - (BOOL)isOpaque {
399   // Make this control opaque so that sub-pixel anti-aliasing works when
400   // CoreAnimation is enabled.
401   return YES;
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];
412   if ([self window]) {
413     // The new window may have different main window status.
414     // This happens when the view is moved into a TabWindowOverlayWindow for
415     // tab dragging.
416     [self windowDidChangeActive];
417   }
420 // ThemedWindowDrawing implementation.
422 - (void)windowDidChangeTheme {
423   [self setNeedsDisplay:YES];
426 - (void)windowDidChangeActive {
427   [self setNeedsDisplay:YES];
430 @end
432 @implementation BookmarkButton(Private)
435 - (void)installCustomTrackingArea {
436   const NSTrackingAreaOptions options =
437       NSTrackingActiveAlways |
438       NSTrackingMouseEnteredAndExited |
439       NSTrackingEnabledDuringMouseDrag;
441   if (area_) {
442     [self removeTrackingArea:area_];
443     [area_ release];
444   }
446   area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
447                                        options:options
448                                          owner:self
449                                       userInfo:nil];
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);
471   [image unlockFocus];
473   return image.autorelease();
476 @end  // @implementation BookmarkButton(Private)