Home | History | Annotate | Download | only in bookmarks
      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.
     17 static const CGFloat kDragImageOpacity = 0.7;
     18 
     19 
     20 namespace bookmark_button {
     21 
     22 NSString* const kPulseBookmarkButtonNotification =
     23     @"PulseBookmarkButtonNotification";
     24 NSString* const kBookmarkKey = @"BookmarkKey";
     25 NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
     26 
     27 };
     28 
     29 namespace {
     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).
     34 BookmarkButton* 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