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