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