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