bookmark_button.mm revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
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.
4
5#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
6
7#include <cmath>
8
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"
19
20using content::UserMetricsAction;
21
22// The opacity of the bookmark button drag image.
23static const CGFloat kDragImageOpacity = 0.7;
24
25
26namespace bookmark_button {
27
28NSString* const kPulseBookmarkButtonNotification =
29    @"PulseBookmarkButtonNotification";
30NSString* const kBookmarkKey = @"BookmarkKey";
31NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
32
33};
34
35namespace {
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).
40BookmarkButton* gDraggedButton = nil; // Weak
41};
42
43@interface BookmarkButton(Private)
44
45// Make a drag image for the button.
46- (NSImage*)dragImage;
47
48- (void)installCustomTrackingArea;
49
50@end  // @interface BookmarkButton(Private)
51
52
53@implementation BookmarkButton
54
55@synthesize delegate = delegate_;
56@synthesize acceptsTrackIn = acceptsTrackIn_;
57
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;
67}
68
69- (void)dealloc {
70  if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
71    [[self cell] safelyStopPulsing];
72  view_id_util::UnsetID(self);
73
74  if (area_) {
75    [self removeTrackingArea:area_];
76    [area_ release];
77  }
78
79  [super dealloc];
80}
81
82- (const BookmarkNode*)bookmarkNode {
83  return [[self cell] bookmarkNode];
84}
85
86- (BOOL)isFolder {
87  const BookmarkNode* node = [self bookmarkNode];
88  return (node && node->is_folder());
89}
90
91- (BOOL)isEmpty {
92  return [self bookmarkNode] ? NO : YES;
93}
94
95- (void)setIsContinuousPulsing:(BOOL)flag {
96  [[self cell] setIsContinuousPulsing:flag];
97}
98
99- (BOOL)isContinuousPulsing {
100  return [[self cell] isContinuousPulsing];
101}
102
103- (NSPoint)screenLocationForRemoveAnimation {
104  NSPoint point;
105
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  }
122
123  return point;
124}
125
126
127- (void)updateTrackingAreas {
128  [self installCustomTrackingArea];
129  [super updateTrackingAreas];
130}
131
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.
138
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  }
147
148  return kDraggableButtonImplUseBase;
149}
150
151
152// By default, NSButton ignores middle-clicks.
153// But we want them.
154- (void)otherMouseUp:(NSEvent*)event {
155  [self performClick:self];
156}
157
158- (BOOL)acceptsTrackInFrom:(id)sender {
159  return  [self isFolder] || [self acceptsTrackIn];
160}
161
162
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;
169
170  if (![self delegate]) {
171    NOTREACHED();
172    return;
173  }
174
175  if ([self isFolder]) {
176    // Close the folder's drop-down menu if it's visible.
177    [[self target] closeBookmarkFolder:self];
178  }
179
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];
184
185  // Ask our delegate to fill the pasteboard for us.
186  NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
187  [[self delegate] fillPasteboard:pboard forDragOfButton:self];
188
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  }
207
208  dragMouseOffset_ = [self convertPoint:[event locationInWindow] fromView:nil];
209  dragPending_ = YES;
210  gDraggedButton = self;
211
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];
219
220  // And we're done.
221  dragPending_ = NO;
222  gDraggedButton = nil;
223
224  [self autorelease];
225}
226
227// Overridden to release bar visibility.
228- (DraggableButtonResult)endDrag {
229  gDraggedButton = nil;
230
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;
236
237  return kDraggableButtonImplUseBase;
238}
239
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;
249}
250
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  }
264}
265
266- (DraggableButtonResult)performMouseDownAction:(NSEvent*)theEvent {
267  int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
268      NSLeftMouseDraggedMask;
269
270  BOOL keepGoing = YES;
271  [[self target] performSelector:[self action] withObject:self];
272  self.draggableButton.actionHasFired = YES;
273
274  DraggableButton* insideBtn = nil;
275
276  while (keepGoing) {
277    theEvent = [[self window] nextEventMatchingMask:eventMask];
278    if (!theEvent)
279      continue;
280
281    NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
282                                 fromView:nil];
283    BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
284
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];
318
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;
333}
334
335
336
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];
343}
344
345// See comments above mouseEntered:.
346- (void)mouseExited:(NSEvent*)event {
347  [delegate_ mouseExitedButton:self event:event];
348}
349
350- (void)mouseMoved:(NSEvent*)theEvent {
351  if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
352    [id(delegate_) mouseMoved:theEvent];
353}
354
355- (void)mouseDragged:(NSEvent*)theEvent {
356  if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
357    [id(delegate_) mouseDragged:theEvent];
358}
359
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];
367
368  // Keep a ref to |self|, in case -rightMouseDown: deletes this bookmark.
369  base::scoped_nsobject<BookmarkButton> keepAlive([self retain]);
370  [super rightMouseDown:event];
371
372  if (![cell isMouseReallyInside]) {
373    [cell setMouseInside:NO animate:YES];
374    [delegate_ mouseExitedButton:self event:event];
375  }
376}
377
378+ (BookmarkButton*)draggedButton {
379  return gDraggedButton;
380}
381
382- (BOOL)canBecomeKeyView {
383  if (![super canBecomeKeyView])
384    return NO;
385
386  // If button is an item in a folder menu, don't become key.
387  return ![[self cell] isFolderButtonCell];
388}
389
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;
404}
405
406- (BOOL)isOpaque {
407  // Make this control opaque so that sub pixel anti aliasing works when core
408  // animation is enabled.
409  return YES;
410}
411
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];
418
419    NSAffineTransform* transform = [NSAffineTransform transform];
420    [transform translateXBy:-NSMinX(frame) yBy:-NSMinY(frame)];
421    [transform concat];
422
423    [toolbarView drawRect:[toolbarView bounds]];
424  }
425
426  [super drawRect:rect];
427}
428
429@end
430
431@implementation BookmarkButton(Private)
432
433
434- (void)installCustomTrackingArea {
435  const NSTrackingAreaOptions options =
436      NSTrackingActiveAlways |
437      NSTrackingMouseEnteredAndExited |
438      NSTrackingEnabledDuringMouseDrag;
439
440  if (area_) {
441    [self removeTrackingArea:area_];
442    [area_ release];
443  }
444
445  area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
446                                       options:options
447                                         owner:self
448                                      userInfo:nil];
449  [self addTrackingArea:area_];
450}
451
452
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]];
458
459  NSGraphicsContext* context = [NSGraphicsContext currentContext];
460  CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
461  CGContextBeginTransparencyLayer(cgContext, 0);
462  CGContextSetAlpha(cgContext, kDragImageOpacity);
463
464  GradientButtonCell* cell =
465      base::mac::ObjCCastStrict<GradientButtonCell>([self cell]);
466  [[cell clipPathForFrame:bounds inView:self] setClip];
467  [cell drawWithFrame:bounds inView:self];
468
469  CGContextEndTransparencyLayer(cgContext);
470  [image unlockFocus];
471
472  return image.autorelease();
473}
474
475@end  // @implementation BookmarkButton(Private)
476