• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2011 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 "base/logging.h"
8#import "base/memory/scoped_nsobject.h"
9#include "chrome/browser/bookmarks/bookmark_model.h"
10#include "chrome/browser/metrics/user_metrics.h"
11#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
12#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
13#import "chrome/browser/ui/cocoa/browser_window_controller.h"
14#import "chrome/browser/ui/cocoa/view_id_util.h"
15
16// The opacity of the bookmark button drag image.
17static const CGFloat kDragImageOpacity = 0.7;
18
19
20namespace bookmark_button {
21
22NSString* const kPulseBookmarkButtonNotification =
23    @"PulseBookmarkButtonNotification";
24NSString* const kBookmarkKey = @"BookmarkKey";
25NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
26
27};
28
29namespace {
30// We need a class variable to track the current dragged button to enable
31// proper live animated dragging behavior, and can't do it in the
32// delegate/controller since you can drag a button from one domain to the
33// other (from a "folder" menu, to the main bar, or vice versa).
34BookmarkButton* gDraggedButton = nil; // Weak
35};
36
37@interface BookmarkButton(Private)
38
39// Make a drag image for the button.
40- (NSImage*)dragImage;
41
42- (void)installCustomTrackingArea;
43
44@end  // @interface BookmarkButton(Private)
45
46
47@implementation BookmarkButton
48
49@synthesize delegate = delegate_;
50@synthesize acceptsTrackIn = acceptsTrackIn_;
51
52- (id)initWithFrame:(NSRect)frameRect {
53  // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
54  // BookmarkBarController, so we can't just override -viewID method to return
55  // it.
56  if ((self = [super initWithFrame:frameRect])) {
57    view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
58    [self installCustomTrackingArea];
59  }
60  return self;
61}
62
63- (void)dealloc {
64  if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
65    [[self cell] safelyStopPulsing];
66  view_id_util::UnsetID(self);
67
68  if (area_) {
69    [self removeTrackingArea:area_];
70    [area_ release];
71  }
72
73  [super dealloc];
74}
75
76- (const BookmarkNode*)bookmarkNode {
77  return [[self cell] bookmarkNode];
78}
79
80- (BOOL)isFolder {
81  const BookmarkNode* node = [self bookmarkNode];
82  return (node && node->is_folder());
83}
84
85- (BOOL)isEmpty {
86  return [self bookmarkNode] ? NO : YES;
87}
88
89- (void)setIsContinuousPulsing:(BOOL)flag {
90  [[self cell] setIsContinuousPulsing:flag];
91}
92
93- (BOOL)isContinuousPulsing {
94  return [[self cell] isContinuousPulsing];
95}
96
97- (NSPoint)screenLocationForRemoveAnimation {
98  NSPoint point;
99
100  if (dragPending_) {
101    // Use the position of the mouse in the drag image as the location.
102    point = dragEndScreenLocation_;
103    point.x += dragMouseOffset_.x;
104    if ([self isFlipped]) {
105      point.y += [self bounds].size.height - dragMouseOffset_.y;
106    } else {
107      point.y += dragMouseOffset_.y;
108    }
109  } else {
110    // Use the middle of this button as the location.
111    NSRect bounds = [self bounds];
112    point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
113    point = [self convertPoint:point toView:nil];
114    point = [[self window] convertBaseToScreen:point];
115  }
116
117  return point;
118}
119
120
121- (void)updateTrackingAreas {
122  [self installCustomTrackingArea];
123  [super updateTrackingAreas];
124}
125
126- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
127                                   yDelta:(float)yDelta
128                              xHysteresis:(float)xHysteresis
129                              yHysteresis:(float)yHysteresis {
130  const float kDownProportion = 1.4142135f; // Square root of 2.
131
132  // We want to show a folder menu when you drag down on folder buttons,
133  // so don't classify this as a drag for that case.
134  if ([self isFolder] &&
135      (yDelta <= -yHysteresis) && // Bottom of hysteresis box was hit.
136      (ABS(yDelta)/ABS(xDelta)) >= kDownProportion)
137    return NO;
138
139  return [super deltaIndicatesDragStartWithXDelta:xDelta
140                                           yDelta:yDelta
141                                      xHysteresis:xHysteresis
142                                      yHysteresis:yHysteresis];
143}
144
145
146// By default, NSButton ignores middle-clicks.
147// But we want them.
148- (void)otherMouseUp:(NSEvent*)event {
149  [self performClick:self];
150}
151
152- (BOOL)acceptsTrackInFrom:(id)sender {
153  return  [self isFolder] || [self acceptsTrackIn];
154}
155
156
157// Overridden from DraggableButton.
158- (void)beginDrag:(NSEvent*)event {
159  // Don't allow a drag of the empty node.
160  // The empty node is a placeholder for "(empty)", to be revisited.
161  if ([self isEmpty])
162    return;
163
164  if (![self delegate]) {
165    NOTREACHED();
166    return;
167  }
168
169  if ([self isFolder]) {
170    // Close the folder's drop-down menu if it's visible.
171    [[self target] closeBookmarkFolder:self];
172  }
173
174  // At the moment, moving bookmarks causes their buttons (like me!)
175  // to be destroyed and rebuilt.  Make sure we don't go away while on
176  // the stack.
177  [self retain];
178
179  // Ask our delegate to fill the pasteboard for us.
180  NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
181  [[self delegate] fillPasteboard:pboard forDragOfButton:self];
182
183  // Lock bar visibility, forcing the overlay to stay visible if we are in
184  // fullscreen mode.
185  if ([[self delegate] dragShouldLockBarVisibility]) {
186    DCHECK(!visibilityDelegate_);
187    NSWindow* window = [[self delegate] browserWindow];
188    visibilityDelegate_ =
189        [BrowserWindowController browserWindowControllerForWindow:window];
190    [visibilityDelegate_ lockBarVisibilityForOwner:self
191                                     withAnimation:NO
192                                             delay:NO];
193  }
194  const BookmarkNode* node = [self bookmarkNode];
195  const BookmarkNode* parent = node ? node->parent() : NULL;
196  if (parent && parent->type() == BookmarkNode::FOLDER) {
197    UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
198  } else {
199    UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
200  }
201
202  dragMouseOffset_ = [self convertPointFromBase:[event locationInWindow]];
203  dragPending_ = YES;
204  gDraggedButton = self;
205
206  CGFloat yAt = [self bounds].size.height;
207  NSSize dragOffset = NSMakeSize(0.0, 0.0);
208  NSImage* image = [self dragImage];
209  [self setHidden:YES];
210  [self dragImage:image at:NSMakePoint(0, yAt) offset:dragOffset
211            event:event pasteboard:pboard source:self slideBack:YES];
212  [self setHidden:NO];
213
214  // And we're done.
215  dragPending_ = NO;
216  gDraggedButton = nil;
217
218  [self autorelease];
219}
220
221// Overridden to release bar visibility.
222- (void)endDrag {
223  gDraggedButton = nil;
224
225  // visibilityDelegate_ can be nil if we're detached, and that's fine.
226  [visibilityDelegate_ releaseBarVisibilityForOwner:self
227                                      withAnimation:YES
228                                              delay:YES];
229  visibilityDelegate_ = nil;
230  [super endDrag];
231}
232
233- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
234  NSDragOperation operation = NSDragOperationCopy;
235  if (isLocal) {
236    operation |= NSDragOperationMove;
237  }
238  if ([delegate_ canDragBookmarkButtonToTrash:self]) {
239    operation |= NSDragOperationDelete;
240  }
241  return operation;
242}
243
244- (void)draggedImage:(NSImage *)anImage
245             endedAt:(NSPoint)aPoint
246           operation:(NSDragOperation)operation {
247  gDraggedButton = nil;
248  // Inform delegate of drag source that we're finished dragging,
249  // so it can close auto-opened bookmark folders etc.
250  [delegate_ bookmarkDragDidEnd:self
251                      operation:operation];
252  // Tell delegate if it should delete us.
253  if (operation & NSDragOperationDelete) {
254    dragEndScreenLocation_ = aPoint;
255    [delegate_ didDragBookmarkToTrash:self];
256  }
257}
258
259- (void)performMouseDownAction:(NSEvent*)theEvent {
260  int eventMask = NSLeftMouseUpMask | NSMouseEnteredMask | NSMouseExitedMask |
261      NSLeftMouseDraggedMask;
262
263  BOOL keepGoing = YES;
264  [[self target] performSelector:[self action] withObject:self];
265  self.actionHasFired = YES;
266
267  DraggableButton* insideBtn = nil;
268
269  while (keepGoing) {
270    theEvent = [[self window] nextEventMatchingMask:eventMask];
271    if (!theEvent)
272      continue;
273
274    NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
275                                 fromView:nil];
276    BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
277
278    switch ([theEvent type]) {
279      case NSMouseEntered:
280      case NSMouseExited: {
281        NSView* trackedView = (NSView*)[[theEvent trackingArea] owner];
282        if (trackedView && [trackedView isKindOfClass:[self class]]) {
283          BookmarkButton* btn = static_cast<BookmarkButton*>(trackedView);
284          if (![btn acceptsTrackInFrom:self])
285            break;
286          if ([theEvent type] == NSMouseEntered) {
287            [[NSCursor arrowCursor] set];
288            [[btn cell] mouseEntered:theEvent];
289            insideBtn = btn;
290          } else {
291            [[btn cell] mouseExited:theEvent];
292            if (insideBtn == btn)
293              insideBtn = nil;
294          }
295        }
296        break;
297      }
298      case NSLeftMouseDragged: {
299        if (insideBtn)
300          [insideBtn mouseDragged:theEvent];
301        break;
302      }
303      case NSLeftMouseUp: {
304        self.durationMouseWasDown = [theEvent timestamp] - self.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];
310
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}
325
326
327
328// mouseEntered: and mouseExited: are called from our
329// BookmarkButtonCell.  We redirect this information to our delegate.
330// The controller can then perform menu-like actions (e.g. "hover over
331// to open menu").
332- (void)mouseEntered:(NSEvent*)event {
333  [delegate_ mouseEnteredButton:self event:event];
334}
335
336// See comments above mouseEntered:.
337- (void)mouseExited:(NSEvent*)event {
338  [delegate_ mouseExitedButton:self event:event];
339}
340
341- (void)mouseMoved:(NSEvent*)theEvent {
342  if ([delegate_ respondsToSelector:@selector(mouseMoved:)])
343    [id(delegate_) mouseMoved:theEvent];
344}
345
346- (void)mouseDragged:(NSEvent*)theEvent {
347  if ([delegate_ respondsToSelector:@selector(mouseDragged:)])
348    [id(delegate_) mouseDragged:theEvent];
349}
350
351+ (BookmarkButton*)draggedButton {
352  return gDraggedButton;
353}
354
355// This only gets called after a click that wasn't a drag, and only on folders.
356- (void)secondaryMouseUpAction:(BOOL)wasInside {
357  const NSTimeInterval kShortClickLength = 0.5;
358  // Long clicks that end over the folder button result in the menu hiding.
359  if (wasInside && ([self durationMouseWasDown] > kShortClickLength)) {
360    [[self target] performSelector:[self action] withObject:self];
361  } else {
362    // Mouse tracked out of button during menu track. Hide menus.
363    if (!wasInside)
364      [delegate_ bookmarkDragDidEnd:self
365                          operation:NSDragOperationNone];
366  }
367}
368
369@end
370
371@implementation BookmarkButton(Private)
372
373
374- (void)installCustomTrackingArea {
375  const NSTrackingAreaOptions options =
376      NSTrackingActiveAlways |
377      NSTrackingMouseEnteredAndExited |
378      NSTrackingEnabledDuringMouseDrag;
379
380  if (area_) {
381    [self removeTrackingArea:area_];
382    [area_ release];
383  }
384
385  area_ = [[NSTrackingArea alloc] initWithRect:[self bounds]
386                                       options:options
387                                         owner:self
388                                      userInfo:nil];
389  [self addTrackingArea:area_];
390}
391
392
393- (NSImage*)dragImage {
394  NSRect bounds = [self bounds];
395
396  // Grab the image from the screen and put it in an |NSImage|. We can't use
397  // this directly since we need to clip it and set its opacity. This won't work
398  // if the source view is clipped. Fortunately, we don't display clipped
399  // bookmark buttons.
400  [self lockFocus];
401  scoped_nsobject<NSBitmapImageRep>
402      bitmap([[NSBitmapImageRep alloc] initWithFocusedViewRect:bounds]);
403  [self unlockFocus];
404  scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:[bitmap size]]);
405  [image addRepresentation:bitmap];
406
407  // Make an autoreleased |NSImage|, which will be returned, and draw into it.
408  // By default, the |NSImage| will be completely transparent.
409  NSImage* dragImage =
410      [[[NSImage alloc] initWithSize:[bitmap size]] autorelease];
411  [dragImage lockFocus];
412
413  // Draw the image with the appropriate opacity, clipping it tightly.
414  GradientButtonCell* cell = static_cast<GradientButtonCell*>([self cell]);
415  DCHECK([cell isKindOfClass:[GradientButtonCell class]]);
416  [[cell clipPathForFrame:bounds inView:self] setClip];
417  [image drawAtPoint:NSMakePoint(0, 0)
418            fromRect:NSMakeRect(0, 0, NSWidth(bounds), NSHeight(bounds))
419           operation:NSCompositeSourceOver
420            fraction:kDragImageOpacity];
421
422  [dragImage unlockFocus];
423  return dragImage;
424}
425
426@end  // @implementation BookmarkButton(Private)
427