Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / bookmarks / bookmark_button.mm
blob39747636cb590386b8f75c4b518d8b4c3ecccf70
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 #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";
35 namespace {
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
61   // it.
62   if ((self = [super initWithFrame:frameRect])) {
63     view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
64     [self installCustomTrackingArea];
65   }
66   return self;
69 - (void)dealloc {
70   if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
71     [[self cell] safelyStopPulsing];
72   view_id_util::UnsetID(self);
74   if (area_) {
75     [self removeTrackingArea:area_];
76     [area_ release];
77   }
79   [super dealloc];
82 - (const BookmarkNode*)bookmarkNode {
83   return [[self cell] bookmarkNode];
86 - (BOOL)isFolder {
87   const BookmarkNode* node = [self bookmarkNode];
88   return (node && node->is_folder());
91 - (BOOL)isEmpty {
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 {
104   NSPoint point;
106   if (dragPending_) {
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;
112     } else {
113       point.y += dragMouseOffset_.y;
114     }
115   } else {
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];
121   }
123   return point;
127 - (void)updateTrackingAreas {
128   [self installCustomTrackingArea];
129   [super updateTrackingAreas];
132 - (DraggableButtonResult)deltaIndicatesDragStartWithXDelta:(float)xDelta
133                                                     yDelta:(float)yDelta
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) {
144     *result = NO;
145     return kDraggableButtonMixinDidWork;
146   }
148   return kDraggableButtonImplUseBase;
152 // By default, NSButton ignores middle-clicks.
153 // But we want them.
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.
167   if ([self isEmpty])
168     return;
170   if (![self delegate]) {
171     NOTREACHED();
172     return;
173   }
175   if ([self isFolder]) {
176     // Close the folder's drop-down menu if it's visible.
177     [[self target] closeBookmarkFolder:self];
178   }
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
182   // the stack.
183   [self retain];
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
190   // fullscreen mode.
191   if ([[self delegate] dragShouldLockBarVisibility]) {
192     DCHECK(!visibilityDelegate_);
193     NSWindow* window = [[self delegate] browserWindow];
194     visibilityDelegate_ =
195         [BrowserWindowController browserWindowControllerForWindow:window];
196     [visibilityDelegate_ lockBarVisibilityForOwner:self
197                                      withAnimation:NO
198                                              delay:NO];
199   }
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"));
204   } else {
205     content::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
206   }
208   dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
209   dragPending_ = YES;
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];
218   [self setHidden:NO];
220   // And we're done.
221   dragPending_ = NO;
222   gDraggedButton = nil;
224   [self autorelease];
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
233                                       withAnimation:YES
234                                               delay:YES];
235   visibilityDelegate_ = nil;
237   return kDraggableButtonImplUseBase;
240 - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
241   NSDragOperation operation = NSDragOperationCopy;
242   if (isLocal) {
243     operation |= NSDragOperationMove;
244   }
245   if ([delegate_ canDragBookmarkButtonToTrash:self]) {
246     operation |= NSDragOperationDelete;
247   }
248   return operation;
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];
263   }
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;
276   while (keepGoing) {
277     theEvent = [[self window] nextEventMatchingMask:eventMask];
278     if (!theEvent)
279       continue;
281     NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
282                                  fromView:nil];
283     BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
285     switch ([theEvent type]) {
286       case NSMouseEntered:
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])
292             break;
293           if ([theEvent type] == NSMouseEntered) {
294             [[NSCursor arrowCursor] set];
295             [[btn cell] mouseEntered:theEvent];
296             insideBtn = btn;
297           } else {
298             [[btn cell] mouseExited:theEvent];
299             if (insideBtn == btn)
300               insideBtn = nil;
301           }
302         }
303         break;
304       }
305       case NSLeftMouseDragged: {
306         if (insideBtn)
307           [insideBtn mouseDragged:theEvent];
308         break;
309       }
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];
319         } else {
320           [self secondaryMouseUpAction:isInside];
321           [[self cell] mouseExited:theEvent];
322           [[insideBtn cell] mouseExited:theEvent];
323         }
324         keepGoing = NO;
325         break;
326       }
327       default:
328         /* Ignore any other kind of event. */
329         break;
330     }
331   }
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
340 // to open menu").
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];
375   }
378 + (BookmarkButton*)draggedButton {
379   return gDraggedButton;
382 - (BOOL)canBecomeKeyView {
383   if (![super canBecomeKeyView])
384     return NO;
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.
394   if (wasInside &&
395       self.draggableButton.durationMouseWasDown > kShortClickLength) {
396     [[self target] performSelector:[self action] withObject:self];
397   } else {
398     // Mouse tracked out of button during menu track. Hide menus.
399     if (!wasInside)
400       [delegate_ bookmarkDragDidEnd:self
401                           operation:NSDragOperationNone];
402   }
403   return kDraggableButtonMixinDidWork;
406 - (BOOL)isOpaque {
407   // Make this control opaque so that sub pixel anti aliasing works when core
408   // animation is enabled.
409   return YES;
412 - (void)drawRect:(NSRect)rect {
413   // Draw the toolbar background.
414   {
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)];
421     [transform concat];
423     [toolbarView drawRect:[toolbarView bounds]];
424   }
426   [super drawRect:rect];
429 @end
431 @implementation BookmarkButton(Private)
434 - (void)installCustomTrackingArea {
435   const NSTrackingAreaOptions options =
436       NSTrackingActiveAlways |
437       NSTrackingMouseEnteredAndExited |
438       NSTrackingEnabledDuringMouseDrag;
440   if (area_) {
441     [self removeTrackingArea:area_];
442     [area_ release];
443   }
445   area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
446                                        options:options
447                                          owner:self
448                                       userInfo:nil];
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);
470   [image unlockFocus];
472   return image.autorelease();
475 @end  // @implementation BookmarkButton(Private)