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