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