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_bar_controller.h" 6 7 #include "app/mac/nsimage_cache.h" 8 #include "base/mac/mac_util.h" 9 #include "base/metrics/histogram.h" 10 #include "base/sys_string_conversions.h" 11 #include "chrome/browser/bookmarks/bookmark_editor.h" 12 #include "chrome/browser/bookmarks/bookmark_model.h" 13 #include "chrome/browser/bookmarks/bookmark_utils.h" 14 #include "chrome/browser/extensions/extension_service.h" 15 #include "chrome/browser/metrics/user_metrics.h" 16 #include "chrome/browser/prefs/pref_service.h" 17 #include "chrome/browser/profiles/profile.h" 18 #import "chrome/browser/themes/theme_service.h" 19 #import "chrome/browser/themes/theme_service_factory.h" 20 #include "chrome/browser/ui/browser.h" 21 #include "chrome/browser/ui/browser_list.h" 22 #import "chrome/browser/ui/cocoa/background_gradient_view.h" 23 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" 24 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" 25 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" 26 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" 27 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h" 28 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" 29 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" 30 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" 31 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" 32 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h" 33 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" 34 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h" 35 #import "chrome/browser/ui/cocoa/browser_window_controller.h" 36 #import "chrome/browser/ui/cocoa/event_utils.h" 37 #import "chrome/browser/ui/cocoa/fullscreen_controller.h" 38 #import "chrome/browser/ui/cocoa/menu_button.h" 39 #import "chrome/browser/ui/cocoa/themed_window.h" 40 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" 41 #import "chrome/browser/ui/cocoa/view_id_util.h" 42 #import "chrome/browser/ui/cocoa/view_resizer.h" 43 #include "chrome/common/extensions/extension_constants.h" 44 #include "chrome/common/pref_names.h" 45 #include "content/browser/tab_contents/tab_contents.h" 46 #include "content/browser/tab_contents/tab_contents_view.h" 47 #include "grit/app_resources.h" 48 #include "grit/generated_resources.h" 49 #include "grit/theme_resources.h" 50 #include "skia/ext/skia_utils_mac.h" 51 #include "ui/base/l10n/l10n_util_mac.h" 52 #include "ui/base/resource/resource_bundle.h" 53 #include "ui/gfx/image.h" 54 55 // Bookmark bar state changing and animations 56 // 57 // The bookmark bar has three real states: "showing" (a normal bar attached to 58 // the toolbar), "hidden", and "detached" (pretending to be part of the web 59 // content on the NTP). It can, or at least should be able to, animate between 60 // these states. There are several complications even without animation: 61 // - The placement of the bookmark bar is done by the BWC, and it needs to know 62 // the state in order to place the bookmark bar correctly (immediately below 63 // the toolbar when showing, below the infobar when detached). 64 // - The "divider" (a black line) needs to be drawn by either the toolbar (when 65 // the bookmark bar is hidden or detached) or by the bookmark bar (when it is 66 // showing). It should not be drawn by both. 67 // - The toolbar needs to vertically "compress" when the bookmark bar is 68 // showing. This ensures the proper display of both the bookmark bar and the 69 // toolbar, and gives a padded area around the bookmark bar items for right 70 // clicks, etc. 71 // 72 // Our model is that the BWC controls us and also the toolbar. We try not to 73 // talk to the browser nor the toolbar directly, instead centralizing control in 74 // the BWC. The key method by which the BWC controls us is 75 // |-updateAndShowNormalBar:showDetachedBar:withAnimation:|. This invokes state 76 // changes, and at appropriate times we request that the BWC do things for us 77 // via either the resize delegate or our general delegate. If the BWC needs any 78 // information about what it should do, or tell the toolbar to do, it can then 79 // query us back (e.g., |-isShownAs...|, |-getDesiredToolbarHeightCompression|, 80 // |-toolbarDividerOpacity|, etc.). 81 // 82 // Animation-related complications: 83 // - Compression of the toolbar is touchy during animation. It must not be 84 // compressed while the bookmark bar is animating to/from showing (from/to 85 // hidden), otherwise it would look like the bookmark bar's contents are 86 // sliding out of the controls inside the toolbar. As such, we have to make 87 // sure that the bookmark bar is shown at the right location and at the 88 // right height (at various points in time). 89 // - Showing the divider is also complicated during animation between hidden 90 // and showing. We have to make sure that the toolbar does not show the 91 // divider despite the fact that it's not compressed. The exception to this 92 // is at the beginning/end of the animation when the toolbar is still 93 // uncompressed but the bookmark bar has height 0. If we're not careful, we 94 // get a flicker at this point. 95 // - We have to ensure that we do the right thing if we're told to change state 96 // while we're running an animation. The generic/easy thing to do is to jump 97 // to the end state of our current animation, and (if the new state change 98 // again involves an animation) begin the new animation. We can do better 99 // than that, however, and sometimes just change the current animation to go 100 // to the new end state (e.g., by "reversing" the animation in the showing -> 101 // hidden -> showing case). We also have to ensure that demands to 102 // immediately change state are always honoured. 103 // 104 // Pointers to animation logic: 105 // - |-moveToVisualState:withAnimation:| starts animations, deciding which ones 106 // we know how to handle. 107 // - |-doBookmarkBarAnimation| has most of the actual logic. 108 // - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain 109 // related logic. 110 // - The BWC's |-layoutSubviews| needs to know how to position things. 111 // - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and 112 // |-bookmarkBar:willAnimateFromState:toState:| in order to inform the 113 // toolbar of required changes. 114 115 namespace { 116 117 // Overlap (in pixels) between the toolbar and the bookmark bar (when showing in 118 // normal mode). 119 const CGFloat kBookmarkBarOverlap = 3.0; 120 121 // Duration of the bookmark bar animations. 122 const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; 123 124 void RecordAppLaunch(Profile* profile, GURL url) { 125 DCHECK(profile->GetExtensionService()); 126 if (!profile->GetExtensionService()->IsInstalledApp(url)) 127 return; 128 129 UMA_HISTOGRAM_ENUMERATION(extension_misc::kAppLaunchHistogram, 130 extension_misc::APP_LAUNCH_BOOKMARK_BAR, 131 extension_misc::APP_LAUNCH_BUCKET_BOUNDARY); 132 } 133 134 } // namespace 135 136 @interface BookmarkBarController(Private) 137 138 // Determines the appropriate state for the given situation. 139 + (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar 140 showDetachedBar:(BOOL)showDetachedBar; 141 142 // Moves to the given next state (from the current state), possibly animating. 143 // If |animate| is NO, it will stop any running animation and jump to the given 144 // state. If YES, it may either (depending on implementation) jump to the end of 145 // the current animation and begin the next one, or stop the current animation 146 // mid-flight and animate to the next state. 147 - (void)moveToVisualState:(bookmarks::VisualState)nextVisualState 148 withAnimation:(BOOL)animate; 149 150 // Return the backdrop to the bookmark bar as various types. 151 - (BackgroundGradientView*)backgroundGradientView; 152 - (AnimatableView*)animatableView; 153 154 // Create buttons for all items in the given bookmark node tree. 155 // Modifies self->buttons_. Do not add more buttons than will fit on the view. 156 - (void)addNodesToButtonList:(const BookmarkNode*)node; 157 158 // Create an autoreleased button appropriate for insertion into the bookmark 159 // bar. Update |xOffset| with the offset appropriate for the subsequent button. 160 - (BookmarkButton*)buttonForNode:(const BookmarkNode*)node 161 xOffset:(int*)xOffset; 162 163 // Puts stuff into the final visual state without animating, stopping a running 164 // animation if necessary. 165 - (void)finalizeVisualState; 166 167 // Stops any current animation in its tracks (midway). 168 - (void)stopCurrentAnimation; 169 170 // Show/hide the bookmark bar. 171 // if |animate| is YES, the changes are made using the animator; otherwise they 172 // are made immediately. 173 - (void)showBookmarkBarWithAnimation:(BOOL)animate; 174 175 // Handles animating the resize of the content view. Returns YES if it handled 176 // the animation, NO if not (and hence it should be done instantly). 177 - (BOOL)doBookmarkBarAnimation; 178 179 // |point| is in the base coordinate system of the destination window; 180 // it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be 181 // made and inserted into the new location while leaving the bookmark in 182 // the old location, otherwise move the bookmark by removing from its old 183 // location and inserting into the new location. 184 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode 185 to:(NSPoint)point 186 copy:(BOOL)copy; 187 188 // Returns the index in the model for a drag to the location given by 189 // |point|. This is determined by finding the first button before the center 190 // of which |point| falls, scanning left to right. Note that, currently, only 191 // the x-coordinate of |point| is considered. Though not currently implemented, 192 // we may check for errors, in which case this would return negative value; 193 // callers should check for this. 194 - (int)indexForDragToPoint:(NSPoint)point; 195 196 // Add or remove buttons to/from the bar until it is filled but not overflowed. 197 - (void)redistributeButtonsOnBarAsNeeded; 198 199 // Determine the nature of the bookmark bar contents based on the number of 200 // buttons showing. If too many then show the off-the-side list, if none 201 // then show the no items label. 202 - (void)reconfigureBookmarkBar; 203 204 - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu; 205 - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu; 206 - (void)tagEmptyMenu:(NSMenu*)menu; 207 - (void)clearMenuTagMap; 208 - (int)preferredHeight; 209 - (void)addNonBookmarkButtonsToView; 210 - (void)addButtonsToView; 211 - (void)centerNoItemsLabel; 212 - (void)setNodeForBarMenu; 213 - (void)watchForExitEvent:(BOOL)watch; 214 - (void)resetAllButtonPositionsWithAnimation:(BOOL)animate; 215 - (BOOL)animationEnabled; 216 217 @end 218 219 @implementation BookmarkBarController 220 221 @synthesize visualState = visualState_; 222 @synthesize lastVisualState = lastVisualState_; 223 @synthesize delegate = delegate_; 224 225 - (id)initWithBrowser:(Browser*)browser 226 initialWidth:(float)initialWidth 227 delegate:(id<BookmarkBarControllerDelegate>)delegate 228 resizeDelegate:(id<ViewResizer>)resizeDelegate { 229 if ((self = [super initWithNibName:@"BookmarkBar" 230 bundle:base::mac::MainAppBundle()])) { 231 // Initialize to an invalid state. 232 visualState_ = bookmarks::kInvalidState; 233 lastVisualState_ = bookmarks::kInvalidState; 234 235 browser_ = browser; 236 initialWidth_ = initialWidth; 237 bookmarkModel_ = browser_->profile()->GetBookmarkModel(); 238 buttons_.reset([[NSMutableArray alloc] init]); 239 delegate_ = delegate; 240 resizeDelegate_ = resizeDelegate; 241 folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]); 242 243 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 244 folderImage_.reset( 245 [rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]); 246 defaultImage_.reset([app::mac::GetCachedImageWithName(@"nav.pdf") retain]); 247 248 // Register for theme changes, bookmark button pulsing, ... 249 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 250 [defaultCenter addObserver:self 251 selector:@selector(themeDidChangeNotification:) 252 name:kBrowserThemeDidChangeNotification 253 object:nil]; 254 [defaultCenter addObserver:self 255 selector:@selector(pulseBookmarkNotification:) 256 name:bookmark_button::kPulseBookmarkButtonNotification 257 object:nil]; 258 259 // This call triggers an awakeFromNib, which builds the bar, which 260 // might uses folderImage_. So make sure it happens after 261 // folderImage_ is loaded. 262 [[self animatableView] setResizeDelegate:resizeDelegate]; 263 } 264 return self; 265 } 266 267 // Can be overridden in a test subclass if a simplistic test is being confused 268 // by asynchronous animation or is running needlessly slow. 269 - (BOOL)animationEnabled { 270 return YES; 271 } 272 273 - (void)pulseBookmarkNotification:(NSNotification*)notification { 274 NSDictionary* dict = [notification userInfo]; 275 const BookmarkNode* node = NULL; 276 NSValue *value = [dict objectForKey:bookmark_button::kBookmarkKey]; 277 DCHECK(value); 278 if (value) 279 node = static_cast<const BookmarkNode*>([value pointerValue]); 280 NSNumber* number = [dict 281 objectForKey:bookmark_button::kBookmarkPulseFlagKey]; 282 DCHECK(number); 283 BOOL doPulse = number ? [number boolValue] : NO; 284 285 // 3 cases: 286 // button on the bar: flash it 287 // button in "other bookmarks" folder: flash other bookmarks 288 // button in "off the side" folder: flash the chevron 289 for (BookmarkButton* button in [self buttons]) { 290 if ([button bookmarkNode] == node) { 291 [button setIsContinuousPulsing:doPulse]; 292 return; 293 } 294 } 295 if ([otherBookmarksButton_ bookmarkNode] == node) { 296 [otherBookmarksButton_ setIsContinuousPulsing:doPulse]; 297 return; 298 } 299 if (node->parent() == bookmarkModel_->GetBookmarkBarNode()) { 300 [offTheSideButton_ setIsContinuousPulsing:doPulse]; 301 return; 302 } 303 304 NOTREACHED() << "no bookmark button found to pulse!"; 305 } 306 307 - (void)dealloc { 308 // We better stop any in-flight animation if we're being killed. 309 [[self animatableView] stopAnimation]; 310 311 // Remove our view from its superview so it doesn't attempt to reference 312 // it when the controller is gone. 313 //TODO(dmaclach): Remove -- http://crbug.com/25845 314 [[self view] removeFromSuperview]; 315 316 // Be sure there is no dangling pointer. 317 if ([[self view] respondsToSelector:@selector(setController:)]) 318 [[self view] performSelector:@selector(setController:) withObject:nil]; 319 320 // For safety, make sure the buttons can no longer call us. 321 for (BookmarkButton* button in buttons_.get()) { 322 [button setDelegate:nil]; 323 [button setTarget:nil]; 324 [button setAction:nil]; 325 } 326 327 bridge_.reset(NULL); 328 [[NSNotificationCenter defaultCenter] removeObserver:self]; 329 [self watchForExitEvent:NO]; 330 [super dealloc]; 331 } 332 333 - (void)awakeFromNib { 334 // We default to NOT open, which means height=0. 335 DCHECK([[self view] isHidden]); // Hidden so it's OK to change. 336 337 // Set our initial height to zero, since that is what the superview 338 // expects. We will resize ourselves open later if needed. 339 [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)]; 340 341 // Complete init of the "off the side" button, as much as we can. 342 [offTheSideButton_ setDraggable:NO]; 343 [offTheSideButton_ setActsOnMouseDown:YES]; 344 345 // We are enabled by default. 346 barIsEnabled_ = YES; 347 348 // Remember the original sizes of the 'no items' and 'import bookmarks' 349 // fields to aid in resizing when the window frame changes. 350 originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame]; 351 originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame]; 352 353 // To make life happier when the bookmark bar is floating, the chevron is a 354 // child of the button view. 355 [offTheSideButton_ removeFromSuperview]; 356 [buttonView_ addSubview:offTheSideButton_]; 357 358 // Copy the bar menu so we know if it's from the bar or a folder. 359 // Then we set its represented item to be the bookmark bar. 360 buttonFolderContextMenu_.reset([[[self view] menu] copy]); 361 362 // When resized we may need to add new buttons, or remove them (if 363 // no longer visible), or add/remove the "off the side" menu. 364 [[self view] setPostsFrameChangedNotifications:YES]; 365 [[NSNotificationCenter defaultCenter] 366 addObserver:self 367 selector:@selector(frameDidChange) 368 name:NSViewFrameDidChangeNotification 369 object:[self view]]; 370 371 // Watch for things going to or from fullscreen. 372 [[NSNotificationCenter defaultCenter] 373 addObserver:self 374 selector:@selector(willEnterOrLeaveFullscreen:) 375 name:kWillEnterFullscreenNotification 376 object:nil]; 377 [[NSNotificationCenter defaultCenter] 378 addObserver:self 379 selector:@selector(willEnterOrLeaveFullscreen:) 380 name:kWillLeaveFullscreenNotification 381 object:nil]; 382 383 // Don't pass ourself along (as 'self') until our init is completely 384 // done. Thus, this call is (almost) last. 385 bridge_.reset(new BookmarkBarBridge(self, bookmarkModel_)); 386 } 387 388 // Called by our main view (a BookmarkBarView) when it gets moved to a 389 // window. We perform operations which need to know the relevant 390 // window (e.g. watch for a window close) so they can't be performed 391 // earlier (such as in awakeFromNib). 392 - (void)viewDidMoveToWindow { 393 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 394 395 // Remove any existing notifications before registering for new ones. 396 [defaultCenter removeObserver:self 397 name:NSWindowWillCloseNotification 398 object:nil]; 399 [defaultCenter removeObserver:self 400 name:NSWindowDidResignKeyNotification 401 object:nil]; 402 403 [defaultCenter addObserver:self 404 selector:@selector(parentWindowWillClose:) 405 name:NSWindowWillCloseNotification 406 object:[[self view] window]]; 407 [defaultCenter addObserver:self 408 selector:@selector(parentWindowDidResignMain:) 409 name:NSWindowDidResignMainNotification 410 object:[[self view] window]]; 411 } 412 413 // When going fullscreen we can run into trouble. Our view is removed 414 // from the non-fullscreen window before the non-fullscreen window 415 // loses key, so our parentDidResignKey: callback never gets called. 416 // In addition, a bookmark folder controller needs to be autoreleased 417 // (in case it's in the event chain when closed), but the release 418 // implicitly needs to happen while it's connected to the original 419 // (non-fullscreen) window to "unlock bar visibility". Such a 420 // contract isn't honored when going fullscreen with the menu option 421 // (not with the keyboard shortcut). We fake it as best we can here. 422 // We have a similar problem leaving fullscreen. 423 - (void)willEnterOrLeaveFullscreen:(NSNotification*)notification { 424 if (folderController_) { 425 [self childFolderWillClose:folderController_]; 426 [self closeFolderAndStopTrackingMenus]; 427 } 428 } 429 430 // NSNotificationCenter callback. 431 - (void)parentWindowWillClose:(NSNotification*)notification { 432 [self closeFolderAndStopTrackingMenus]; 433 } 434 435 // NSNotificationCenter callback. 436 - (void)parentWindowDidResignMain:(NSNotification*)notification { 437 [self closeFolderAndStopTrackingMenus]; 438 } 439 440 // Change the layout of the bookmark bar's subviews in response to a visibility 441 // change (e.g., show or hide the bar) or style change (attached or floating). 442 - (void)layoutSubviews { 443 NSRect frame = [[self view] frame]; 444 NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame)); 445 446 // The state of our morph (if any); 1 is total bubble, 0 is the regular bar. 447 CGFloat morph = [self detachedMorphProgress]; 448 449 // Add padding to the detached bookmark bar. 450 buttonViewFrame = NSInsetRect(buttonViewFrame, 451 morph * bookmarks::kNTPBookmarkBarPadding, 452 morph * bookmarks::kNTPBookmarkBarPadding); 453 454 [buttonView_ setFrame:buttonViewFrame]; 455 } 456 457 // We don't change a preference; we only change visibility. Preference changing 458 // (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We 459 // simply update based on what we're told. 460 - (void)updateVisibility { 461 [self showBookmarkBarWithAnimation:NO]; 462 } 463 464 - (void)setBookmarkBarEnabled:(BOOL)enabled { 465 if (enabled != barIsEnabled_) { 466 barIsEnabled_ = enabled; 467 [self updateVisibility]; 468 } 469 } 470 471 - (CGFloat)getDesiredToolbarHeightCompression { 472 // Some special cases.... 473 if (!barIsEnabled_) 474 return 0; 475 476 if ([self isAnimationRunning]) { 477 // No toolbar compression when animating between hidden and showing, nor 478 // between showing and detached. 479 if ([self isAnimatingBetweenState:bookmarks::kHiddenState 480 andState:bookmarks::kShowingState] || 481 [self isAnimatingBetweenState:bookmarks::kShowingState 482 andState:bookmarks::kDetachedState]) 483 return 0; 484 485 // If we ever need any other animation cases, code would go here. 486 } 487 488 return [self isInState:bookmarks::kShowingState] ? kBookmarkBarOverlap : 0; 489 } 490 491 - (CGFloat)toolbarDividerOpacity { 492 // Some special cases.... 493 if ([self isAnimationRunning]) { 494 // In general, the toolbar shouldn't show a divider while we're animating 495 // between showing and hidden. The exception is when our height is < 1, in 496 // which case we can't draw it. It's all-or-nothing (no partial opacity). 497 if ([self isAnimatingBetweenState:bookmarks::kHiddenState 498 andState:bookmarks::kShowingState]) 499 return (NSHeight([[self view] frame]) < 1) ? 1 : 0; 500 501 // The toolbar should show the divider when animating between showing and 502 // detached (but opacity will vary). 503 if ([self isAnimatingBetweenState:bookmarks::kShowingState 504 andState:bookmarks::kDetachedState]) 505 return static_cast<CGFloat>([self detachedMorphProgress]); 506 507 // If we ever need any other animation cases, code would go here. 508 } 509 510 // In general, only show the divider when it's in the normal showing state. 511 return [self isInState:bookmarks::kShowingState] ? 0 : 1; 512 } 513 514 - (NSImage*)faviconForNode:(const BookmarkNode*)node { 515 if (!node) 516 return defaultImage_; 517 518 if (node->is_folder()) 519 return folderImage_; 520 521 const SkBitmap& favicon = bookmarkModel_->GetFavicon(node); 522 if (!favicon.isNull()) 523 return gfx::SkBitmapToNSImage(favicon); 524 525 return defaultImage_; 526 } 527 528 - (void)closeFolderAndStopTrackingMenus { 529 showFolderMenus_ = NO; 530 [self closeAllBookmarkFolders]; 531 } 532 533 - (BOOL)canEditBookmarks { 534 PrefService* prefs = browser_->profile()->GetPrefs(); 535 return prefs->GetBoolean(prefs::kEditBookmarksEnabled); 536 } 537 538 - (BOOL)canEditBookmark:(const BookmarkNode*)node { 539 // Don't allow edit/delete of the bar node, or of "Other Bookmarks" 540 if ((node == nil) || 541 (node == bookmarkModel_->other_node()) || 542 (node == bookmarkModel_->GetBookmarkBarNode())) 543 return NO; 544 return YES; 545 } 546 547 #pragma mark Actions 548 549 // Helper methods called on the main thread by runMenuFlashThread. 550 551 - (void)setButtonFlashStateOn:(id)sender { 552 [sender highlight:YES]; 553 } 554 555 - (void)setButtonFlashStateOff:(id)sender { 556 [sender highlight:NO]; 557 } 558 559 -(void)cleanupAfterMenuFlashThread:(id)sender { 560 [self closeFolderAndStopTrackingMenus]; 561 562 // Items retained by doMenuFlashOnSeparateThread below. 563 [sender release]; 564 [self release]; 565 } 566 567 // End runMenuFlashThread helper methods. 568 569 // This call is invoked only by doMenuFlashOnSeparateThread below. 570 // It makes the selected BookmarkButton (which is masquerading as a menu item) 571 // flash a few times to give confirmation feedback, then it closes the menu. 572 // It spends all its time sleeping or scheduling UI work on the main thread. 573 - (void)runMenuFlashThread:(id)sender { 574 575 // Check this is not running on the main thread, as it sleeps. 576 DCHECK(![NSThread isMainThread]); 577 578 // Duration of flash phases and number of flashes designed to evoke a 579 // slightly retro "more mac-like than the Mac" feel. 580 // Current Cocoa UI has a barely perceptible flash,probably because Apple 581 // doesn't fire the action til after the animation and so there's a hurry. 582 // As this code is fully asynchronous, it can take its time. 583 const float kBBOnFlashTime = 0.08; 584 const float kBBOffFlashTime = 0.08; 585 const int kBookmarkButtonMenuFlashes = 3; 586 587 for (int count = 0 ; count < kBookmarkButtonMenuFlashes ; count++) { 588 [self performSelectorOnMainThread:@selector(setButtonFlashStateOn:) 589 withObject:sender 590 waitUntilDone:NO]; 591 [NSThread sleepForTimeInterval:kBBOnFlashTime]; 592 [self performSelectorOnMainThread:@selector(setButtonFlashStateOff:) 593 withObject:sender 594 waitUntilDone:NO]; 595 [NSThread sleepForTimeInterval:kBBOffFlashTime]; 596 } 597 [self performSelectorOnMainThread:@selector(cleanupAfterMenuFlashThread:) 598 withObject:sender 599 waitUntilDone:NO]; 600 } 601 602 // Non-blocking call which starts the process to make the selected menu item 603 // flash a few times to give confirmation feedback, after which it closes the 604 // menu. The item is of course actually a BookmarkButton masquerading as a menu 605 // item). 606 - (void)doMenuFlashOnSeparateThread:(id)sender { 607 608 // Ensure that self and sender don't go away before the animation completes. 609 // These retains are balanced in cleanupAfterMenuFlashThread above. 610 [self retain]; 611 [sender retain]; 612 [NSThread detachNewThreadSelector:@selector(runMenuFlashThread:) 613 toTarget:self 614 withObject:sender]; 615 } 616 617 - (IBAction)openBookmark:(id)sender { 618 BOOL isMenuItem = [[sender cell] isFolderButtonCell]; 619 BOOL animate = isMenuItem && [self animationEnabled]; 620 if (animate) 621 [self doMenuFlashOnSeparateThread:sender]; 622 DCHECK([sender respondsToSelector:@selector(bookmarkNode)]); 623 const BookmarkNode* node = [sender bookmarkNode]; 624 WindowOpenDisposition disposition = 625 event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); 626 RecordAppLaunch(browser_->profile(), node->GetURL()); 627 [self openURL:node->GetURL() disposition:disposition]; 628 629 if (!animate) 630 [self closeFolderAndStopTrackingMenus]; 631 } 632 633 // Common function to open a bookmark folder of any type. 634 - (void)openBookmarkFolder:(id)sender { 635 DCHECK([sender isKindOfClass:[BookmarkButton class]]); 636 DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]); 637 638 showFolderMenus_ = !showFolderMenus_; 639 640 if (sender == offTheSideButton_) 641 [[sender cell] setStartingChildIndex:displayedButtonCount_]; 642 643 // Toggle presentation of bar folder menus. 644 [folderTarget_ openBookmarkFolderFromButton:sender]; 645 } 646 647 648 // Click on a bookmark folder button. 649 - (IBAction)openBookmarkFolderFromButton:(id)sender { 650 [self openBookmarkFolder:sender]; 651 } 652 653 // Click on the "off the side" button (chevron), which opens like a folder 654 // button but isn't exactly a parent folder. 655 - (IBAction)openOffTheSideFolderFromButton:(id)sender { 656 [self openBookmarkFolder:sender]; 657 } 658 659 - (IBAction)openBookmarkInNewForegroundTab:(id)sender { 660 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 661 if (node) 662 [self openURL:node->GetURL() disposition:NEW_FOREGROUND_TAB]; 663 [self closeAllBookmarkFolders]; 664 } 665 666 - (IBAction)openBookmarkInNewWindow:(id)sender { 667 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 668 if (node) 669 [self openURL:node->GetURL() disposition:NEW_WINDOW]; 670 } 671 672 - (IBAction)openBookmarkInIncognitoWindow:(id)sender { 673 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 674 if (node) 675 [self openURL:node->GetURL() disposition:OFF_THE_RECORD]; 676 } 677 678 - (IBAction)editBookmark:(id)sender { 679 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 680 if (!node) 681 return; 682 683 if (node->is_folder()) { 684 BookmarkNameFolderController* controller = 685 [[BookmarkNameFolderController alloc] 686 initWithParentWindow:[[self view] window] 687 profile:browser_->profile() 688 node:node]; 689 [controller runAsModalSheet]; 690 return; 691 } 692 693 // There is no real need to jump to a platform-common routine at 694 // this point (which just jumps back to objc) other than consistency 695 // across platforms. 696 // 697 // TODO(jrg): identify when we NO_TREE. I can see it in the code 698 // for the other platforms but can't find a way to trigger it in the 699 // UI. 700 BookmarkEditor::Show([[self view] window], 701 browser_->profile(), 702 node->parent(), 703 BookmarkEditor::EditDetails(node), 704 BookmarkEditor::SHOW_TREE); 705 } 706 707 - (IBAction)cutBookmark:(id)sender { 708 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 709 if (node) { 710 std::vector<const BookmarkNode*> nodes; 711 nodes.push_back(node); 712 bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, true); 713 } 714 } 715 716 - (IBAction)copyBookmark:(id)sender { 717 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 718 if (node) { 719 std::vector<const BookmarkNode*> nodes; 720 nodes.push_back(node); 721 bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, false); 722 } 723 } 724 725 // Paste the copied node immediately after the node for which the context 726 // menu has been presented if the node is a non-folder bookmark, otherwise 727 // past at the end of the folder node. 728 - (IBAction)pasteBookmark:(id)sender { 729 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 730 if (node) { 731 int index = -1; 732 if (node != bookmarkModel_->GetBookmarkBarNode() && !node->is_folder()) { 733 const BookmarkNode* parent = node->parent(); 734 index = parent->GetIndexOf(node) + 1; 735 if (index > parent->child_count()) 736 index = -1; 737 node = parent; 738 } 739 bookmark_utils::PasteFromClipboard(bookmarkModel_, node, index); 740 } 741 } 742 743 - (IBAction)deleteBookmark:(id)sender { 744 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 745 if (node) { 746 bookmarkModel_->Remove(node->parent(), 747 node->parent()->GetIndexOf(node)); 748 } 749 } 750 751 - (IBAction)openAllBookmarks:(id)sender { 752 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 753 if (node) { 754 [self openAll:node disposition:NEW_FOREGROUND_TAB]; 755 UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarks"), 756 browser_->profile()); 757 } 758 } 759 760 - (IBAction)openAllBookmarksNewWindow:(id)sender { 761 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 762 if (node) { 763 [self openAll:node disposition:NEW_WINDOW]; 764 UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarksNewWindow"), 765 browser_->profile()); 766 } 767 } 768 769 - (IBAction)openAllBookmarksIncognitoWindow:(id)sender { 770 const BookmarkNode* node = [self nodeFromMenuItem:sender]; 771 if (node) { 772 [self openAll:node disposition:OFF_THE_RECORD]; 773 UserMetrics::RecordAction( 774 UserMetricsAction("OpenAllBookmarksIncognitoWindow"), 775 browser_->profile()); 776 } 777 } 778 779 // May be called from the bar or from a folder button. 780 // If called from a button, that button becomes the parent. 781 - (IBAction)addPage:(id)sender { 782 const BookmarkNode* parent = [self nodeFromMenuItem:sender]; 783 if (!parent) 784 parent = bookmarkModel_->GetBookmarkBarNode(); 785 BookmarkEditor::Show([[self view] window], 786 browser_->profile(), 787 parent, 788 BookmarkEditor::EditDetails(), 789 BookmarkEditor::SHOW_TREE); 790 } 791 792 // Might be called from the context menu over the bar OR over a 793 // button. If called from a button, that button becomes a sibling of 794 // the new node. If called from the bar, add to the end of the bar. 795 - (IBAction)addFolder:(id)sender { 796 const BookmarkNode* senderNode = [self nodeFromMenuItem:sender]; 797 const BookmarkNode* parent = NULL; 798 int newIndex = 0; 799 // If triggered from the bar, folder or "others" folder - add as a child to 800 // the end. 801 // If triggered from a bookmark, add as next sibling. 802 BookmarkNode::Type type = senderNode->type(); 803 if (type == BookmarkNode::BOOKMARK_BAR || 804 type == BookmarkNode::OTHER_NODE || 805 type == BookmarkNode::FOLDER) { 806 parent = senderNode; 807 newIndex = parent->child_count(); 808 } else { 809 parent = senderNode->parent(); 810 newIndex = parent->GetIndexOf(senderNode) + 1; 811 } 812 BookmarkNameFolderController* controller = 813 [[BookmarkNameFolderController alloc] 814 initWithParentWindow:[[self view] window] 815 profile:browser_->profile() 816 parent:parent 817 newIndex:newIndex]; 818 [controller runAsModalSheet]; 819 } 820 821 - (IBAction)importBookmarks:(id)sender { 822 browser_->OpenImportSettingsDialog(); 823 } 824 825 #pragma mark Private Methods 826 827 // Called after the current theme has changed. 828 - (void)themeDidChangeNotification:(NSNotification*)aNotification { 829 ui::ThemeProvider* themeProvider = 830 static_cast<ThemeService*>([[aNotification object] pointerValue]); 831 [self updateTheme:themeProvider]; 832 } 833 834 // (Private) Method is the same as [self view], but is provided to be explicit. 835 - (BackgroundGradientView*)backgroundGradientView { 836 DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]); 837 return (BackgroundGradientView*)[self view]; 838 } 839 840 // (Private) Method is the same as [self view], but is provided to be explicit. 841 - (AnimatableView*)animatableView { 842 DCHECK([[self view] isKindOfClass:[AnimatableView class]]); 843 return (AnimatableView*)[self view]; 844 } 845 846 // Position the off-the-side chevron to the left of the otherBookmarks button. 847 - (void)positionOffTheSideButton { 848 NSRect frame = [offTheSideButton_ frame]; 849 if (otherBookmarksButton_.get()) { 850 frame.origin.x = ([otherBookmarksButton_ frame].origin.x - 851 (frame.size.width + 852 bookmarks::kBookmarkHorizontalPadding)); 853 [offTheSideButton_ setFrame:frame]; 854 } 855 } 856 857 // Configure the off-the-side button (e.g. specify the node range, 858 // check if we should enable or disable it, etc). 859 - (void)configureOffTheSideButtonContentsAndVisibility { 860 // If deleting a button while off-the-side is open, buttons may be 861 // promoted from off-the-side to the bar. Accomodate. 862 if (folderController_ && 863 ([folderController_ parentButton] == offTheSideButton_)) { 864 [folderController_ reconfigureMenu]; 865 } 866 867 [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_]; 868 [[offTheSideButton_ cell] 869 setBookmarkNode:bookmarkModel_->GetBookmarkBarNode()]; 870 int bookmarkChildren = bookmarkModel_->GetBookmarkBarNode()->child_count(); 871 if (bookmarkChildren > displayedButtonCount_) { 872 [offTheSideButton_ setHidden:NO]; 873 } else { 874 // If we just deleted the last item in an off-the-side menu so the 875 // button will be going away, make sure the menu goes away. 876 if (folderController_ && 877 ([folderController_ parentButton] == offTheSideButton_)) 878 [self closeAllBookmarkFolders]; 879 // (And hide the button, too.) 880 [offTheSideButton_ setHidden:YES]; 881 } 882 } 883 884 // Main menubar observation code, so we can know to close our fake menus if the 885 // user clicks on the actual menubar, as multiple unconnected menus sharing 886 // the screen looks weird. 887 // Needed because the hookForEvent method doesn't see the click on the menubar. 888 889 // Gets called when the menubar is clicked. 890 - (void)begunTracking:(NSNotification *)notification { 891 [self closeFolderAndStopTrackingMenus]; 892 } 893 894 // Install the callback. 895 - (void)startObservingMenubar { 896 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 897 [nc addObserver:self 898 selector:@selector(begunTracking:) 899 name:NSMenuDidBeginTrackingNotification 900 object:[NSApp mainMenu]]; 901 } 902 903 // Remove the callback. 904 - (void)stopObservingMenubar { 905 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 906 [nc removeObserver:self 907 name:NSMenuDidBeginTrackingNotification 908 object:[NSApp mainMenu]]; 909 } 910 911 // End of menubar observation code. 912 913 // Begin (or end) watching for a click outside this window. Unlike 914 // normal NSWindows, bookmark folder "fake menu" windows do not become 915 // key or main. Thus, traditional notification (e.g. WillResignKey) 916 // won't work. Our strategy is to watch (at the app level) for a 917 // "click outside" these windows to detect when they logically lose 918 // focus. 919 - (void)watchForExitEvent:(BOOL)watch { 920 CrApplication* app = static_cast<CrApplication*>([NSApplication 921 sharedApplication]); 922 DCHECK([app isKindOfClass:[CrApplication class]]); 923 if (watch) { 924 if (!watchingForExitEvent_) { 925 [app addEventHook:self]; 926 [self startObservingMenubar]; 927 } 928 } else { 929 if (watchingForExitEvent_) { 930 [app removeEventHook:self]; 931 [self stopObservingMenubar]; 932 } 933 } 934 watchingForExitEvent_ = watch; 935 } 936 937 // Keep the "no items" label centered in response to a frame size change. 938 - (void)centerNoItemsLabel { 939 // Note that this computation is done in the parent's coordinate system, 940 // which is unflipped. Also, we want the label to be a fixed distance from 941 // the bottom, so that it slides up properly (on animating to hidden). 942 // The textfield sits in the itemcontainer, so to center it we maintain 943 // equal vertical padding on the top and bottom. 944 int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) - 945 NSHeight([[buttonView_ noItemContainer] frame])) / 2; 946 [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)]; 947 } 948 949 // (Private) 950 - (void)showBookmarkBarWithAnimation:(BOOL)animate { 951 if (animate && !ignoreAnimations_) { 952 // If |-doBookmarkBarAnimation| does the animation, we're done. 953 if ([self doBookmarkBarAnimation]) 954 return; 955 956 // Else fall through and do the change instantly. 957 } 958 959 // Set our height. 960 [resizeDelegate_ resizeView:[self view] 961 newHeight:[self preferredHeight]]; 962 963 // Only show the divider if showing the normal bookmark bar. 964 BOOL showsDivider = [self isInState:bookmarks::kShowingState]; 965 [[self backgroundGradientView] setShowsDivider:showsDivider]; 966 967 // Make sure we're shown. 968 [[self view] setHidden:![self isVisible]]; 969 970 // Update everything else. 971 [self layoutSubviews]; 972 [self frameDidChange]; 973 } 974 975 // (Private) 976 - (BOOL)doBookmarkBarAnimation { 977 if ([self isAnimatingFromState:bookmarks::kHiddenState 978 toState:bookmarks::kShowingState]) { 979 [[self backgroundGradientView] setShowsDivider:YES]; 980 [[self view] setHidden:NO]; 981 AnimatableView* view = [self animatableView]; 982 // Height takes into account the extra height we have since the toolbar 983 // only compresses when we're done. 984 [view animateToNewHeight:(bookmarks::kBookmarkBarHeight - 985 kBookmarkBarOverlap) 986 duration:kBookmarkBarAnimationDuration]; 987 } else if ([self isAnimatingFromState:bookmarks::kShowingState 988 toState:bookmarks::kHiddenState]) { 989 [[self backgroundGradientView] setShowsDivider:YES]; 990 [[self view] setHidden:NO]; 991 AnimatableView* view = [self animatableView]; 992 [view animateToNewHeight:0 993 duration:kBookmarkBarAnimationDuration]; 994 } else if ([self isAnimatingFromState:bookmarks::kShowingState 995 toState:bookmarks::kDetachedState]) { 996 [[self backgroundGradientView] setShowsDivider:YES]; 997 [[self view] setHidden:NO]; 998 AnimatableView* view = [self animatableView]; 999 [view animateToNewHeight:bookmarks::kNTPBookmarkBarHeight 1000 duration:kBookmarkBarAnimationDuration]; 1001 } else if ([self isAnimatingFromState:bookmarks::kDetachedState 1002 toState:bookmarks::kShowingState]) { 1003 [[self backgroundGradientView] setShowsDivider:YES]; 1004 [[self view] setHidden:NO]; 1005 AnimatableView* view = [self animatableView]; 1006 // Height takes into account the extra height we have since the toolbar 1007 // only compresses when we're done. 1008 [view animateToNewHeight:(bookmarks::kBookmarkBarHeight - 1009 kBookmarkBarOverlap) 1010 duration:kBookmarkBarAnimationDuration]; 1011 } else { 1012 // Oops! An animation we don't know how to handle. 1013 return NO; 1014 } 1015 1016 return YES; 1017 } 1018 1019 // Enable or disable items. We are the menu delegate for both the bar 1020 // and for bookmark folder buttons. 1021 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem { 1022 // NSUserInterfaceValidations says that the passed-in object has type 1023 // |id<NSValidatedUserInterfaceItem>|, but this function needs to call the 1024 // NSObject method -isKindOfClass: on the parameter. In theory, this is not 1025 // correct, but this is probably a bug in the method signature. 1026 NSMenuItem* item = static_cast<NSMenuItem*>(anItem); 1027 // Yes for everything we don't explicitly deny. 1028 if (![item isKindOfClass:[NSMenuItem class]]) 1029 return YES; 1030 1031 // Yes if we're not a special BookmarkMenu. 1032 if (![[item menu] isKindOfClass:[BookmarkMenu class]]) 1033 return YES; 1034 1035 // No if we think it's a special BookmarkMenu but have trouble. 1036 const BookmarkNode* node = [self nodeFromMenuItem:item]; 1037 if (!node) 1038 return NO; 1039 1040 // If this is the bar menu, we only have things to do if there are 1041 // buttons. If this is a folder button menu, we only have things to 1042 // do if the folder has items. 1043 NSMenu* menu = [item menu]; 1044 BOOL thingsToDo = NO; 1045 if (menu == [[self view] menu]) { 1046 thingsToDo = [buttons_ count] ? YES : NO; 1047 } else { 1048 if (node && node->is_folder() && node->child_count()) { 1049 thingsToDo = YES; 1050 } 1051 } 1052 1053 // Disable openAll* if we have nothing to do. 1054 SEL action = [item action]; 1055 if ((!thingsToDo) && 1056 ((action == @selector(openAllBookmarks:)) || 1057 (action == @selector(openAllBookmarksNewWindow:)) || 1058 (action == @selector(openAllBookmarksIncognitoWindow:)))) { 1059 return NO; 1060 } 1061 1062 bool can_edit = [self canEditBookmarks]; 1063 if ((action == @selector(editBookmark:)) || 1064 (action == @selector(deleteBookmark:)) || 1065 (action == @selector(cutBookmark:)) || 1066 (action == @selector(copyBookmark:))) { 1067 if (![self canEditBookmark:node]) 1068 return NO; 1069 if (action != @selector(copyBookmark:) && !can_edit) 1070 return NO; 1071 } 1072 1073 if (action == @selector(pasteBookmark:) && 1074 (!bookmark_utils::CanPasteFromClipboard(node) || !can_edit)) { 1075 return NO; 1076 } 1077 1078 if ((!can_edit) && 1079 ((action == @selector(addPage:)) || 1080 (action == @selector(addFolder:)))) { 1081 return NO; 1082 } 1083 1084 // If this is an incognito window, don't allow "open in incognito". 1085 if ((action == @selector(openBookmarkInIncognitoWindow:)) || 1086 (action == @selector(openAllBookmarksIncognitoWindow:))) { 1087 Profile* profile = browser_->profile(); 1088 if (profile->IsOffTheRecord() || 1089 !profile->GetPrefs()->GetBoolean(prefs::kIncognitoEnabled)) { 1090 return NO; 1091 } 1092 } 1093 1094 // Enabled by default. 1095 return YES; 1096 } 1097 1098 // Actually open the URL. This is the last chance for a unit test to 1099 // override. 1100 - (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition { 1101 browser_->OpenURL(url, GURL(), disposition, PageTransition::AUTO_BOOKMARK); 1102 } 1103 1104 - (void)clearMenuTagMap { 1105 seedId_ = 0; 1106 menuTagMap_.clear(); 1107 } 1108 1109 - (int)preferredHeight { 1110 DCHECK(![self isAnimationRunning]); 1111 1112 if (!barIsEnabled_) 1113 return 0; 1114 1115 switch (visualState_) { 1116 case bookmarks::kShowingState: 1117 return bookmarks::kBookmarkBarHeight; 1118 case bookmarks::kDetachedState: 1119 return bookmarks::kNTPBookmarkBarHeight; 1120 case bookmarks::kHiddenState: 1121 return 0; 1122 case bookmarks::kInvalidState: 1123 default: 1124 NOTREACHED(); 1125 return 0; 1126 } 1127 } 1128 1129 // Recursively add the given bookmark node and all its children to 1130 // menu, one menu item per node. 1131 - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu { 1132 NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child]; 1133 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title 1134 action:nil 1135 keyEquivalent:@""] autorelease]; 1136 [menu addItem:item]; 1137 [item setImage:[self faviconForNode:child]]; 1138 if (child->is_folder()) { 1139 NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease]; 1140 [menu setSubmenu:submenu forItem:item]; 1141 if (child->child_count()) { 1142 [self addFolderNode:child toMenu:submenu]; // potentially recursive 1143 } else { 1144 [self tagEmptyMenu:submenu]; 1145 } 1146 } else { 1147 [item setTarget:self]; 1148 [item setAction:@selector(openBookmarkMenuItem:)]; 1149 [item setTag:[self menuTagFromNodeId:child->id()]]; 1150 if (child->is_url()) { 1151 // Add a tooltip 1152 std::string url_string = child->GetURL().possibly_invalid_spec(); 1153 NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", 1154 base::SysUTF16ToNSString(child->GetTitle()), 1155 url_string.c_str()]; 1156 [item setToolTip:tooltip]; 1157 } 1158 } 1159 } 1160 1161 // Empty menus are odd; if empty, add something to look at. 1162 // Matches windows behavior. 1163 - (void)tagEmptyMenu:(NSMenu*)menu { 1164 NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU); 1165 [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title 1166 action:NULL 1167 keyEquivalent:@""] autorelease]]; 1168 } 1169 1170 // Add the children of the given bookmark node (and their children...) 1171 // to menu, one menu item per node. 1172 - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu { 1173 for (int i = 0; i < node->child_count(); i++) { 1174 const BookmarkNode* child = node->GetChild(i); 1175 [self addNode:child toMenu:menu]; 1176 } 1177 } 1178 1179 // Return an autoreleased NSMenu that represents the given bookmark 1180 // folder node. 1181 - (NSMenu *)menuForFolderNode:(const BookmarkNode*)node { 1182 if (!node->is_folder()) 1183 return nil; 1184 NSString* title = base::SysUTF16ToNSString(node->GetTitle()); 1185 NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease]; 1186 [self addFolderNode:node toMenu:menu]; 1187 1188 if (![menu numberOfItems]) { 1189 [self tagEmptyMenu:menu]; 1190 } 1191 return menu; 1192 } 1193 1194 // Return an appropriate width for the given bookmark button cell. 1195 // The "+2" is needed because, sometimes, Cocoa is off by a tad. 1196 // Example: for a bookmark named "Moma" or "SFGate", it is one pixel 1197 // too small. For "FBL" it is 2 pixels too small. 1198 // For a bookmark named "SFGateFooWoo", it is just fine. 1199 - (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell { 1200 CGFloat desired = [cell cellSize].width + 2; 1201 return std::min(desired, bookmarks::kDefaultBookmarkWidth); 1202 } 1203 1204 - (IBAction)openBookmarkMenuItem:(id)sender { 1205 int64 tag = [self nodeIdFromMenuTag:[sender tag]]; 1206 const BookmarkNode* node = bookmarkModel_->GetNodeByID(tag); 1207 WindowOpenDisposition disposition = 1208 event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); 1209 [self openURL:node->GetURL() disposition:disposition]; 1210 } 1211 1212 // For the given root node of the bookmark bar, show or hide (as 1213 // appropriate) the "no items" container (text which says "bookmarks 1214 // go here"). 1215 - (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node { 1216 BOOL hideNoItemWarning = node->child_count() > 0; 1217 [[buttonView_ noItemContainer] setHidden:hideNoItemWarning]; 1218 } 1219 1220 // TODO(jrg): write a "build bar" so there is a nice spot for things 1221 // like the contextual menu which is invoked when not over a 1222 // bookmark. On Safari that menu has a "new folder" option. 1223 - (void)addNodesToButtonList:(const BookmarkNode*)node { 1224 [self showOrHideNoItemContainerForNode:node]; 1225 1226 CGFloat maxViewX = NSMaxX([[self view] bounds]); 1227 int xOffset = 0; 1228 for (int i = 0; i < node->child_count(); i++) { 1229 const BookmarkNode* child = node->GetChild(i); 1230 BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; 1231 if (NSMinX([button frame]) >= maxViewX) 1232 break; 1233 [buttons_ addObject:button]; 1234 } 1235 } 1236 1237 - (BookmarkButton*)buttonForNode:(const BookmarkNode*)node 1238 xOffset:(int*)xOffset { 1239 BookmarkButtonCell* cell = [self cellForBookmarkNode:node]; 1240 NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset]; 1241 1242 scoped_nsobject<BookmarkButton> 1243 button([[BookmarkButton alloc] initWithFrame:frame]); 1244 DCHECK(button.get()); 1245 1246 // [NSButton setCell:] warns to NOT use setCell: other than in the 1247 // initializer of a control. However, we are using a basic 1248 // NSButton whose initializer does not take an NSCell as an 1249 // object. To honor the assumed semantics, we do nothing with 1250 // NSButton between alloc/init and setCell:. 1251 [button setCell:cell]; 1252 [button setDelegate:self]; 1253 1254 // We cannot set the button cell's text color until it is placed in 1255 // the button (e.g. the [button setCell:cell] call right above). We 1256 // also cannot set the cell's text color until the view is added to 1257 // the hierarchy. If that second part is now true, set the color. 1258 // (If not we'll set the color on the 1st themeChanged: 1259 // notification.) 1260 ui::ThemeProvider* themeProvider = [[[self view] window] themeProvider]; 1261 if (themeProvider) { 1262 NSColor* color = 1263 themeProvider->GetNSColor(ThemeService::COLOR_BOOKMARK_TEXT, 1264 true); 1265 [cell setTextColor:color]; 1266 } 1267 1268 if (node->is_folder()) { 1269 [button setTarget:self]; 1270 [button setAction:@selector(openBookmarkFolderFromButton:)]; 1271 [button setActsOnMouseDown:YES]; 1272 } else { 1273 // Make the button do something 1274 [button setTarget:self]; 1275 [button setAction:@selector(openBookmark:)]; 1276 if (node->is_url()) { 1277 // Add a tooltip. 1278 NSString* title = base::SysUTF16ToNSString(node->GetTitle()); 1279 std::string url_string = node->GetURL().possibly_invalid_spec(); 1280 NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, 1281 url_string.c_str()]; 1282 [button setToolTip:tooltip]; 1283 } 1284 } 1285 return [[button.get() retain] autorelease]; 1286 } 1287 1288 // Add non-bookmark buttons to the view. This includes the chevron 1289 // and the "other bookmarks" button. Technically "other bookmarks" is 1290 // a bookmark button but it is treated specially. Only needs to be 1291 // called when these buttons are new or when the bookmark bar is 1292 // cleared (e.g. on a loaded: call). Unlike addButtonsToView below, 1293 // we don't need to add/remove these dynamically in response to window 1294 // resize. 1295 - (void)addNonBookmarkButtonsToView { 1296 [buttonView_ addSubview:otherBookmarksButton_.get()]; 1297 [buttonView_ addSubview:offTheSideButton_]; 1298 } 1299 1300 // Add bookmark buttons to the view only if they are completely 1301 // visible and don't overlap the "other bookmarks". Remove buttons 1302 // which are clipped. Called when building the bookmark bar the first time. 1303 - (void)addButtonsToView { 1304 displayedButtonCount_ = 0; 1305 NSMutableArray* buttons = [self buttons]; 1306 for (NSButton* button in buttons) { 1307 if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) - 1308 bookmarks::kBookmarkHorizontalPadding)) 1309 break; 1310 [buttonView_ addSubview:button]; 1311 ++displayedButtonCount_; 1312 } 1313 NSUInteger removalCount = 1314 [buttons count] - (NSUInteger)displayedButtonCount_; 1315 if (removalCount > 0) { 1316 NSRange removalRange = NSMakeRange(displayedButtonCount_, removalCount); 1317 [buttons removeObjectsInRange:removalRange]; 1318 } 1319 } 1320 1321 // Create the button for "Other Bookmarks" on the right of the bar. 1322 - (void)createOtherBookmarksButton { 1323 // Can't create this until the model is loaded, but only need to 1324 // create it once. 1325 if (otherBookmarksButton_.get()) 1326 return; 1327 1328 // TODO(jrg): remove duplicate code 1329 NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()]; 1330 int ignored = 0; 1331 NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:&ignored]; 1332 frame.origin.x = [[self buttonView] bounds].size.width - frame.size.width; 1333 frame.origin.x -= bookmarks::kBookmarkHorizontalPadding; 1334 BookmarkButton* button = [[BookmarkButton alloc] initWithFrame:frame]; 1335 [button setDraggable:NO]; 1336 [button setActsOnMouseDown:YES]; 1337 otherBookmarksButton_.reset(button); 1338 view_id_util::SetID(button, VIEW_ID_OTHER_BOOKMARKS); 1339 1340 // Make sure this button, like all other BookmarkButtons, lives 1341 // until the end of the current event loop. 1342 [[button retain] autorelease]; 1343 1344 // Peg at right; keep same height as bar. 1345 [button setAutoresizingMask:(NSViewMinXMargin)]; 1346 [button setCell:cell]; 1347 [button setDelegate:self]; 1348 [button setTarget:self]; 1349 [button setAction:@selector(openBookmarkFolderFromButton:)]; 1350 [buttonView_ addSubview:button]; 1351 1352 // Now that it's here, move the chevron over. 1353 [self positionOffTheSideButton]; 1354 } 1355 1356 // Now that the model is loaded, set the bookmark bar root as the node 1357 // represented by the bookmark bar (default, background) menu. 1358 - (void)setNodeForBarMenu { 1359 const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); 1360 BookmarkMenu* menu = static_cast<BookmarkMenu*>([[self view] menu]); 1361 1362 // Make sure types are compatible 1363 DCHECK(sizeof(long long) == sizeof(int64)); 1364 [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]]; 1365 } 1366 1367 // To avoid problems with sync, changes that may impact the current 1368 // bookmark (e.g. deletion) make sure context menus are closed. This 1369 // prevents deleting a node which no longer exists. 1370 - (void)cancelMenuTracking { 1371 [buttonContextMenu_ cancelTracking]; 1372 [buttonFolderContextMenu_ cancelTracking]; 1373 } 1374 1375 // Determines the appropriate state for the given situation. 1376 + (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar 1377 showDetachedBar:(BOOL)showDetachedBar { 1378 if (showNormalBar) 1379 return bookmarks::kShowingState; 1380 if (showDetachedBar) 1381 return bookmarks::kDetachedState; 1382 return bookmarks::kHiddenState; 1383 } 1384 1385 - (void)moveToVisualState:(bookmarks::VisualState)nextVisualState 1386 withAnimation:(BOOL)animate { 1387 BOOL isAnimationRunning = [self isAnimationRunning]; 1388 1389 // No-op if the next state is the same as the "current" one, subject to the 1390 // following conditions: 1391 // - no animation is running; or 1392 // - an animation is running and |animate| is YES ([*] if it's NO, we'd want 1393 // to cancel the animation and jump to the final state). 1394 if ((nextVisualState == visualState_) && (!isAnimationRunning || animate)) 1395 return; 1396 1397 // If an animation is running, we want to finalize it. Otherwise we'd have to 1398 // be able to animate starting from the middle of one type of animation. We 1399 // assume that animations that we know about can be "reversed". 1400 if (isAnimationRunning) { 1401 // Don't cancel if we're going to reverse the animation. 1402 if (nextVisualState != lastVisualState_) { 1403 [self stopCurrentAnimation]; 1404 [self finalizeVisualState]; 1405 } 1406 1407 // If we're in case [*] above, we can stop here. 1408 if (nextVisualState == visualState_) 1409 return; 1410 } 1411 1412 // Now update with the new state change. 1413 lastVisualState_ = visualState_; 1414 visualState_ = nextVisualState; 1415 1416 // Animate only if told to and if bar is enabled. 1417 if (animate && !ignoreAnimations_ && barIsEnabled_) { 1418 [self closeAllBookmarkFolders]; 1419 // Take care of any animation cases we know how to handle. 1420 1421 // We know how to handle hidden <-> normal, normal <-> detached.... 1422 if ([self isAnimatingBetweenState:bookmarks::kHiddenState 1423 andState:bookmarks::kShowingState] || 1424 [self isAnimatingBetweenState:bookmarks::kShowingState 1425 andState:bookmarks::kDetachedState]) { 1426 [delegate_ bookmarkBar:self willAnimateFromState:lastVisualState_ 1427 toState:visualState_]; 1428 [self showBookmarkBarWithAnimation:YES]; 1429 return; 1430 } 1431 1432 // If we ever need any other animation cases, code would go here. 1433 // Let any animation cases which we don't know how to handle fall through to 1434 // the unanimated case. 1435 } 1436 1437 // Just jump to the state. 1438 [self finalizeVisualState]; 1439 } 1440 1441 // N.B.: |-moveToVisualState:...| will check if this should be a no-op or not. 1442 - (void)updateAndShowNormalBar:(BOOL)showNormalBar 1443 showDetachedBar:(BOOL)showDetachedBar 1444 withAnimation:(BOOL)animate { 1445 bookmarks::VisualState newVisualState = 1446 [BookmarkBarController visualStateToShowNormalBar:showNormalBar 1447 showDetachedBar:showDetachedBar]; 1448 [self moveToVisualState:newVisualState 1449 withAnimation:animate && !ignoreAnimations_]; 1450 } 1451 1452 // (Private) 1453 - (void)finalizeVisualState { 1454 // We promise that our delegate that the variables will be finalized before 1455 // the call to |-bookmarkBar:didChangeFromState:toState:|. 1456 bookmarks::VisualState oldVisualState = lastVisualState_; 1457 lastVisualState_ = bookmarks::kInvalidState; 1458 1459 // Notify our delegate. 1460 [delegate_ bookmarkBar:self didChangeFromState:oldVisualState 1461 toState:visualState_]; 1462 1463 // Update ourselves visually. 1464 [self updateVisibility]; 1465 } 1466 1467 // (Private) 1468 - (void)stopCurrentAnimation { 1469 [[self animatableView] stopAnimation]; 1470 } 1471 1472 // Delegate method for |AnimatableView| (a superclass of 1473 // |BookmarkBarToolbarView|). 1474 - (void)animationDidEnd:(NSAnimation*)animation { 1475 [self finalizeVisualState]; 1476 } 1477 1478 - (void)reconfigureBookmarkBar { 1479 [self redistributeButtonsOnBarAsNeeded]; 1480 [self positionOffTheSideButton]; 1481 [self configureOffTheSideButtonContentsAndVisibility]; 1482 [self centerNoItemsLabel]; 1483 } 1484 1485 // Determine if the given |view| can completely fit within the constraint of 1486 // maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum 1487 // width. If the minimum width is not achievable then hide the view. Return YES 1488 // if the view was hidden. 1489 - (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX { 1490 BOOL wasHidden = NO; 1491 // See if the view needs to be narrowed. 1492 NSRect frame = [view frame]; 1493 if (NSMaxX(frame) > maxViewX) { 1494 // Resize if more than 30 pixels are showing, otherwise hide. 1495 if (NSMinX(frame) + 30.0 < maxViewX) { 1496 frame.size.width = maxViewX - NSMinX(frame); 1497 [view setFrame:frame]; 1498 } else { 1499 [view setHidden:YES]; 1500 wasHidden = YES; 1501 } 1502 } 1503 return wasHidden; 1504 } 1505 1506 // Adjust the horizontal width and the visibility of the "For quick access" 1507 // text field and "Import bookmarks..." button based on the current width 1508 // of the containing |buttonView_| (which is affected by window width). 1509 - (void)adjustNoItemContainerWidthsForMaxX:(CGFloat)maxViewX { 1510 if (![[buttonView_ noItemContainer] isHidden]) { 1511 // Reset initial frames for the two items, then adjust as necessary. 1512 NSTextField* noItemTextfield = [buttonView_ noItemTextfield]; 1513 [noItemTextfield setFrame:originalNoItemsRect_]; 1514 [noItemTextfield setHidden:NO]; 1515 NSButton* importBookmarksButton = [buttonView_ importBookmarksButton]; 1516 [importBookmarksButton setFrame:originalImportBookmarksRect_]; 1517 [importBookmarksButton setHidden:NO]; 1518 // Check each to see if they need to be shrunk or hidden. 1519 if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX]) 1520 [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX]; 1521 } 1522 } 1523 1524 - (void)redistributeButtonsOnBarAsNeeded { 1525 const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); 1526 NSInteger barCount = node->child_count(); 1527 1528 // Determine the current maximum extent of the visible buttons. 1529 CGFloat maxViewX = NSMaxX([[self view] bounds]); 1530 NSButton* otherBookmarksButton = otherBookmarksButton_.get(); 1531 // If necessary, pull in the width to account for the Other Bookmarks button. 1532 if (otherBookmarksButton_) 1533 maxViewX = [otherBookmarksButton frame].origin.x - 1534 bookmarks::kBookmarkHorizontalPadding; 1535 // If we're already overflowing, then we need to account for the chevron. 1536 if (barCount > displayedButtonCount_) 1537 maxViewX = [offTheSideButton_ frame].origin.x - 1538 bookmarks::kBookmarkHorizontalPadding; 1539 1540 // As a result of pasting or dragging, the bar may now have more buttons 1541 // than will fit so remove any which overflow. They will be shown in 1542 // the off-the-side folder. 1543 while (displayedButtonCount_ > 0) { 1544 BookmarkButton* button = [buttons_ lastObject]; 1545 if (NSMaxX([button frame]) < maxViewX) 1546 break; 1547 [buttons_ removeLastObject]; 1548 [button setDelegate:nil]; 1549 [button removeFromSuperview]; 1550 --displayedButtonCount_; 1551 } 1552 1553 // As a result of cutting, deleting and dragging, the bar may now have room 1554 // for more buttons. 1555 int xOffset = displayedButtonCount_ > 0 ? 1556 NSMaxX([[buttons_ lastObject] frame]) + 1557 bookmarks::kBookmarkHorizontalPadding : 0; 1558 for (int i = displayedButtonCount_; i < barCount; ++i) { 1559 const BookmarkNode* child = node->GetChild(i); 1560 BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; 1561 // If we're testing against the last possible button then account 1562 // for the chevron no longer needing to be shown. 1563 if (i == barCount + 1) 1564 maxViewX += NSWidth([offTheSideButton_ frame]) + 1565 bookmarks::kBookmarkHorizontalPadding; 1566 if (NSMaxX([button frame]) >= maxViewX) 1567 break; 1568 ++displayedButtonCount_; 1569 [buttons_ addObject:button]; 1570 [buttonView_ addSubview:button]; 1571 } 1572 1573 // While we're here, adjust the horizontal width and the visibility 1574 // of the "For quick access" and "Import bookmarks..." text fields. 1575 if (![buttons_ count]) 1576 [self adjustNoItemContainerWidthsForMaxX:maxViewX]; 1577 } 1578 1579 #pragma mark Private Methods Exposed for Testing 1580 1581 - (BookmarkBarView*)buttonView { 1582 return buttonView_; 1583 } 1584 1585 - (NSMutableArray*)buttons { 1586 return buttons_.get(); 1587 } 1588 1589 - (NSButton*)offTheSideButton { 1590 return offTheSideButton_; 1591 } 1592 1593 - (BOOL)offTheSideButtonIsHidden { 1594 return [offTheSideButton_ isHidden]; 1595 } 1596 1597 - (BookmarkButton*)otherBookmarksButton { 1598 return otherBookmarksButton_.get(); 1599 } 1600 1601 - (BookmarkBarFolderController*)folderController { 1602 return folderController_; 1603 } 1604 1605 - (id)folderTarget { 1606 return folderTarget_.get(); 1607 } 1608 1609 - (int)displayedButtonCount { 1610 return displayedButtonCount_; 1611 } 1612 1613 // Delete all buttons (bookmarks, chevron, "other bookmarks") from the 1614 // bookmark bar; reset knowledge of bookmarks. 1615 - (void)clearBookmarkBar { 1616 for (BookmarkButton* button in buttons_.get()) { 1617 [button setDelegate:nil]; 1618 [button removeFromSuperview]; 1619 } 1620 [buttons_ removeAllObjects]; 1621 [self clearMenuTagMap]; 1622 displayedButtonCount_ = 0; 1623 1624 // Make sure there are no stale pointers in the pasteboard. This 1625 // can be important if a bookmark is deleted (via bookmark sync) 1626 // while in the middle of a drag. The "drag completed" code 1627 // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is 1628 // careful enough to bail if there is no data found at "drop" time. 1629 // 1630 // Unfortunately the clearContents selector is 10.6 only. The best 1631 // we can do is make sure something else is present in place of the 1632 // stale bookmark. 1633 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; 1634 [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self]; 1635 [pboard setString:@"" forType:NSStringPboardType]; 1636 } 1637 1638 // Return an autoreleased NSCell suitable for a bookmark button. 1639 // TODO(jrg): move much of the cell config into the BookmarkButtonCell class. 1640 - (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node { 1641 NSImage* image = node ? [self faviconForNode:node] : nil; 1642 NSMenu* menu = node && node->is_folder() ? buttonFolderContextMenu_ : 1643 buttonContextMenu_; 1644 BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node 1645 contextMenu:menu 1646 cellText:nil 1647 cellImage:image]; 1648 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; 1649 1650 // Note: a quirk of setting a cell's text color is that it won't work 1651 // until the cell is associated with a button, so we can't theme the cell yet. 1652 1653 return cell; 1654 } 1655 1656 // Returns a frame appropriate for the given bookmark cell, suitable 1657 // for creating an NSButton that will contain it. |xOffset| is the X 1658 // offset for the frame; it is increased to be an appropriate X offset 1659 // for the next button. 1660 - (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell 1661 xOffset:(int*)xOffset { 1662 DCHECK(xOffset); 1663 NSRect bounds = [buttonView_ bounds]; 1664 bounds.size.height = bookmarks::kBookmarkButtonHeight; 1665 1666 NSRect frame = NSInsetRect(bounds, 1667 bookmarks::kBookmarkHorizontalPadding, 1668 bookmarks::kBookmarkVerticalPadding); 1669 frame.size.width = [self widthForBookmarkButtonCell:cell]; 1670 1671 // Add an X offset based on what we've already done 1672 frame.origin.x += *xOffset; 1673 1674 // And up the X offset for next time. 1675 *xOffset = NSMaxX(frame); 1676 1677 return frame; 1678 } 1679 1680 // A bookmark button's contents changed. Check for growth 1681 // (e.g. increase the width up to the maximum). If we grew, move 1682 // other bookmark buttons over. 1683 - (void)checkForBookmarkButtonGrowth:(NSButton*)button { 1684 NSRect frame = [button frame]; 1685 CGFloat desiredSize = [self widthForBookmarkButtonCell:[button cell]]; 1686 CGFloat delta = desiredSize - frame.size.width; 1687 if (delta) { 1688 frame.size.width = desiredSize; 1689 [button setFrame:frame]; 1690 for (NSButton* button in buttons_.get()) { 1691 NSRect buttonFrame = [button frame]; 1692 if (buttonFrame.origin.x > frame.origin.x) { 1693 buttonFrame.origin.x += delta; 1694 [button setFrame:buttonFrame]; 1695 } 1696 } 1697 } 1698 // We may have just crossed a threshold to enable the off-the-side 1699 // button. 1700 [self configureOffTheSideButtonContentsAndVisibility]; 1701 } 1702 1703 // Called when our controlled frame has changed size. 1704 - (void)frameDidChange { 1705 if (!bookmarkModel_->IsLoaded()) 1706 return; 1707 [self updateTheme:[[[self view] window] themeProvider]]; 1708 [self reconfigureBookmarkBar]; 1709 } 1710 1711 // Given a NSMenuItem tag, return the appropriate bookmark node id. 1712 - (int64)nodeIdFromMenuTag:(int32)tag { 1713 return menuTagMap_[tag]; 1714 } 1715 1716 // Create and return a new tag for the given node id. 1717 - (int32)menuTagFromNodeId:(int64)menuid { 1718 int tag = seedId_++; 1719 menuTagMap_[tag] = menuid; 1720 return tag; 1721 } 1722 1723 // Return the BookmarkNode associated with the given NSMenuItem. Can 1724 // return NULL which means "do nothing". One case where it would 1725 // return NULL is if the bookmark model gets modified while you have a 1726 // context menu open. 1727 - (const BookmarkNode*)nodeFromMenuItem:(id)sender { 1728 const BookmarkNode* node = NULL; 1729 BookmarkMenu* menu = (BookmarkMenu*)[sender menu]; 1730 if ([menu isKindOfClass:[BookmarkMenu class]]) { 1731 int64 id = [menu id]; 1732 node = bookmarkModel_->GetNodeByID(id); 1733 } 1734 return node; 1735 } 1736 1737 // Adapt appearance of buttons to the current theme. Called after 1738 // theme changes, or when our view is added to the view hierarchy. 1739 // Oddly, the view pings us instead of us pinging our view. This is 1740 // because our trigger is an [NSView viewWillMoveToWindow:], which the 1741 // controller doesn't normally know about. Otherwise we don't have 1742 // access to the theme before we know what window we will be on. 1743 - (void)updateTheme:(ui::ThemeProvider*)themeProvider { 1744 if (!themeProvider) 1745 return; 1746 NSColor* color = 1747 themeProvider->GetNSColor(ThemeService::COLOR_BOOKMARK_TEXT, 1748 true); 1749 for (BookmarkButton* button in buttons_.get()) { 1750 BookmarkButtonCell* cell = [button cell]; 1751 [cell setTextColor:color]; 1752 } 1753 [[otherBookmarksButton_ cell] setTextColor:color]; 1754 } 1755 1756 // Return YES if the event indicates an exit from the bookmark bar 1757 // folder menus. E.g. "click outside" of the area we are watching. 1758 // At this time we are watching the area that includes all popup 1759 // bookmark folder windows. 1760 - (BOOL)isEventAnExitEvent:(NSEvent*)event { 1761 NSWindow* eventWindow = [event window]; 1762 NSWindow* myWindow = [[self view] window]; 1763 switch ([event type]) { 1764 case NSLeftMouseDown: 1765 case NSRightMouseDown: 1766 // If the click is in my window but NOT in the bookmark bar, consider 1767 // it a click 'outside'. Clicks directly on an active button (i.e. one 1768 // that is a folder and for which its folder menu is showing) are 'in'. 1769 // All other clicks on the bookmarks bar are counted as 'outside' 1770 // because they should close any open bookmark folder menu. 1771 if (eventWindow == myWindow) { 1772 NSView* hitView = 1773 [[eventWindow contentView] hitTest:[event locationInWindow]]; 1774 if (hitView == [folderController_ parentButton]) 1775 return NO; 1776 if (![hitView isDescendantOf:[self view]] || hitView == buttonView_) 1777 return YES; 1778 } 1779 // If a click in a bookmark bar folder window and that isn't 1780 // one of my bookmark bar folders, YES is click outside. 1781 if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow 1782 class]]) { 1783 return YES; 1784 } 1785 break; 1786 case NSKeyDown: { 1787 bool result = NO; 1788 // Event hooks often see the same keydown event twice due to the way key 1789 // events get dispatched and redispatched, so ignore if this keydown 1790 // event has the EXACT same timestamp as the previous keydown. 1791 static NSTimeInterval lastKeyDownEventTime; 1792 NSTimeInterval thisTime = [event timestamp]; 1793 if (lastKeyDownEventTime != thisTime) { 1794 lastKeyDownEventTime = thisTime; 1795 if (folderController_) { 1796 result = [folderController_ handleInputText:[event characters]]; 1797 } 1798 } 1799 return result; 1800 } 1801 case NSKeyUp: 1802 return NO; 1803 case NSLeftMouseDragged: 1804 // We can get here with the following sequence: 1805 // - open a bookmark folder 1806 // - right-click (and unclick) on it to open context menu 1807 // - move mouse to window titlebar then click-drag it by the titlebar 1808 // http://crbug.com/49333 1809 return NO; 1810 default: 1811 break; 1812 } 1813 return NO; 1814 } 1815 1816 #pragma mark Drag & Drop 1817 1818 // Find something like std::is_between<T>? I can't believe one doesn't exist. 1819 static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { 1820 return ((value >= low) && (value <= high)); 1821 } 1822 1823 // Return the proposed drop target for a hover open button from the 1824 // given array, or nil if none. We use this for distinguishing 1825 // between a hover-open candidate or drop-indicator draw. 1826 // Helper for buttonForDroppingOnAtPoint:. 1827 // Get UI review on "middle half" ness. 1828 // http://crbug.com/36276 1829 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point 1830 fromArray:(NSArray*)array { 1831 for (BookmarkButton* button in array) { 1832 // Break early if we've gone too far. 1833 if ((NSMinX([button frame]) > point.x) || (![button superview])) 1834 return nil; 1835 // Careful -- this only applies to the bar with horiz buttons. 1836 // Intentionally NOT using NSPointInRect() so that scrolling into 1837 // a submenu doesn't cause it to be closed. 1838 if (ValueInRangeInclusive(NSMinX([button frame]), 1839 point.x, 1840 NSMaxX([button frame]))) { 1841 // Over a button but let's be a little more specific (make sure 1842 // it's over the middle half, not just over it). 1843 NSRect frame = [button frame]; 1844 NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0); 1845 if (ValueInRangeInclusive(NSMinX(middleHalfOfButton), 1846 point.x, 1847 NSMaxX(middleHalfOfButton))) { 1848 // It makes no sense to drop on a non-folder; there is no hover. 1849 if (![button isFolder]) 1850 return nil; 1851 // Got it! 1852 return button; 1853 } else { 1854 // Over a button but not over the middle half. 1855 return nil; 1856 } 1857 } 1858 } 1859 // Not hovering over a button. 1860 return nil; 1861 } 1862 1863 // Return the proposed drop target for a hover open button, or nil if 1864 // none. Works with both the bookmark buttons and the "Other 1865 // Bookmarks" button. Point is in [self view] coordinates. 1866 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { 1867 point = [[self view] convertPoint:point 1868 fromView:[[[self view] window] contentView]]; 1869 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point 1870 fromArray:buttons_.get()]; 1871 // One more chance -- try "Other Bookmarks" and "off the side" (if visible). 1872 // This is different than BookmarkBarFolderController. 1873 if (!button) { 1874 NSMutableArray* array = [NSMutableArray array]; 1875 if (![self offTheSideButtonIsHidden]) 1876 [array addObject:offTheSideButton_]; 1877 [array addObject:otherBookmarksButton_]; 1878 button = [self buttonForDroppingOnAtPoint:point 1879 fromArray:array]; 1880 } 1881 return button; 1882 } 1883 1884 - (int)indexForDragToPoint:(NSPoint)point { 1885 // TODO(jrg): revisit position info based on UI team feedback. 1886 // dropLocation is in bar local coordinates. 1887 NSPoint dropLocation = 1888 [[self view] convertPoint:point 1889 fromView:[[[self view] window] contentView]]; 1890 BookmarkButton* buttonToTheRightOfDraggedButton = nil; 1891 for (BookmarkButton* button in buttons_.get()) { 1892 CGFloat midpoint = NSMidX([button frame]); 1893 if (dropLocation.x <= midpoint) { 1894 buttonToTheRightOfDraggedButton = button; 1895 break; 1896 } 1897 } 1898 if (buttonToTheRightOfDraggedButton) { 1899 const BookmarkNode* afterNode = 1900 [buttonToTheRightOfDraggedButton bookmarkNode]; 1901 DCHECK(afterNode); 1902 int index = afterNode->parent()->GetIndexOf(afterNode); 1903 // Make sure we don't get confused by buttons which aren't visible. 1904 return std::min(index, displayedButtonCount_); 1905 } 1906 1907 // If nothing is to my right I am at the end! 1908 return displayedButtonCount_; 1909 } 1910 1911 // TODO(mrossetti,jrg): Yet more duplicated code. 1912 // http://crbug.com/35966 1913 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode 1914 to:(NSPoint)point 1915 copy:(BOOL)copy { 1916 DCHECK(sourceNode); 1917 // Drop destination. 1918 const BookmarkNode* destParent = NULL; 1919 int destIndex = 0; 1920 1921 // First check if we're dropping on a button. If we have one, and 1922 // it's a folder, drop in it. 1923 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; 1924 if ([button isFolder]) { 1925 destParent = [button bookmarkNode]; 1926 // Drop it at the end. 1927 destIndex = [button bookmarkNode]->child_count(); 1928 } else { 1929 // Else we're dropping somewhere on the bar, so find the right spot. 1930 destParent = bookmarkModel_->GetBookmarkBarNode(); 1931 destIndex = [self indexForDragToPoint:point]; 1932 } 1933 1934 // Be sure we don't try and drop a folder into itself. 1935 if (sourceNode != destParent) { 1936 if (copy) 1937 bookmarkModel_->Copy(sourceNode, destParent, destIndex); 1938 else 1939 bookmarkModel_->Move(sourceNode, destParent, destIndex); 1940 } 1941 1942 [self closeFolderAndStopTrackingMenus]; 1943 1944 // Movement of a node triggers observers (like us) to rebuild the 1945 // bar so we don't have to do so explicitly. 1946 1947 return YES; 1948 } 1949 1950 - (void)draggingEnded:(id<NSDraggingInfo>)info { 1951 [self closeFolderAndStopTrackingMenus]; 1952 [[BookmarkButton draggedButton] setHidden:NO]; 1953 [self resetAllButtonPositionsWithAnimation:YES]; 1954 } 1955 1956 // Set insertionPos_ and hasInsertionPos_, and make insertion space for a 1957 // hypothetical drop with the new button having a left edge of |where|. 1958 // Gets called only by our view. 1959 - (void)setDropInsertionPos:(CGFloat)where { 1960 BOOL animate = [self animationEnabled]; 1961 if (!hasInsertionPos_ || where != insertionPos_) { 1962 insertionPos_ = where; 1963 hasInsertionPos_ = YES; 1964 CGFloat left = bookmarks::kBookmarkHorizontalPadding; 1965 CGFloat paddingWidth = bookmarks::kDefaultBookmarkWidth; 1966 BookmarkButton* draggedButton = [BookmarkButton draggedButton]; 1967 if (draggedButton) { 1968 paddingWidth = std::min(bookmarks::kDefaultBookmarkWidth, 1969 NSWidth([draggedButton frame])); 1970 } 1971 // Put all the buttons where they belong, with all buttons to the right 1972 // of the insertion point shuffling right to make space for it. 1973 for (NSButton* button in buttons_.get()) { 1974 // Hidden buttons get no space. 1975 if ([button isHidden]) 1976 continue; 1977 NSRect buttonFrame = [button frame]; 1978 buttonFrame.origin.x = left; 1979 // Update "left" for next time around. 1980 left += buttonFrame.size.width; 1981 if (left > insertionPos_) 1982 buttonFrame.origin.x += paddingWidth; 1983 left += bookmarks::kBookmarkHorizontalPadding; 1984 if (animate) 1985 [[button animator] setFrame:buttonFrame]; 1986 else 1987 [button setFrame:buttonFrame]; 1988 } 1989 } 1990 } 1991 1992 // Put all visible bookmark bar buttons in their normal locations, either with 1993 // or without animation according to the |animate| flag. 1994 // This is generally useful, so is called from various places internally. 1995 - (void)resetAllButtonPositionsWithAnimation:(BOOL)animate { 1996 CGFloat left = bookmarks::kBookmarkHorizontalPadding; 1997 animate &= [self animationEnabled]; 1998 1999 for (NSButton* button in buttons_.get()) { 2000 // Hidden buttons get no space. 2001 if ([button isHidden]) 2002 continue; 2003 NSRect buttonFrame = [button frame]; 2004 buttonFrame.origin.x = left; 2005 left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding; 2006 if (animate) 2007 [[button animator] setFrame:buttonFrame]; 2008 else 2009 [button setFrame:buttonFrame]; 2010 } 2011 } 2012 2013 // Clear insertion flag, remove insertion space and put all visible bookmark 2014 // bar buttons in their normal locations. 2015 // Gets called only by our view. 2016 - (void)clearDropInsertionPos { 2017 if (hasInsertionPos_) { 2018 hasInsertionPos_ = NO; 2019 [self resetAllButtonPositionsWithAnimation:YES]; 2020 } 2021 } 2022 2023 #pragma mark Bridge Notification Handlers 2024 2025 // TODO(jrg): for now this is brute force. 2026 - (void)loaded:(BookmarkModel*)model { 2027 DCHECK(model == bookmarkModel_); 2028 if (!model->IsLoaded()) 2029 return; 2030 2031 // If this is a rebuild request while we have a folder open, close it. 2032 // TODO(mrossetti): Eliminate the need for this because it causes the folder 2033 // menu to disappear after a cut/copy/paste/delete change. 2034 // See: http://crbug.com/36614 2035 if (folderController_) 2036 [self closeAllBookmarkFolders]; 2037 2038 // Brute force nuke and build. 2039 savedFrameWidth_ = NSWidth([[self view] frame]); 2040 const BookmarkNode* node = model->GetBookmarkBarNode(); 2041 [self clearBookmarkBar]; 2042 [self addNodesToButtonList:node]; 2043 [self createOtherBookmarksButton]; 2044 [self updateTheme:[[[self view] window] themeProvider]]; 2045 [self positionOffTheSideButton]; 2046 [self addNonBookmarkButtonsToView]; 2047 [self addButtonsToView]; 2048 [self configureOffTheSideButtonContentsAndVisibility]; 2049 [self setNodeForBarMenu]; 2050 [self reconfigureBookmarkBar]; 2051 } 2052 2053 - (void)beingDeleted:(BookmarkModel*)model { 2054 // The browser may be being torn down; little is safe to do. As an 2055 // example, it may not be safe to clear the pasteboard. 2056 // http://crbug.com/38665 2057 } 2058 2059 - (void)nodeAdded:(BookmarkModel*)model 2060 parent:(const BookmarkNode*)newParent index:(int)newIndex { 2061 // If a context menu is open, close it. 2062 [self cancelMenuTracking]; 2063 2064 const BookmarkNode* newNode = newParent->GetChild(newIndex); 2065 id<BookmarkButtonControllerProtocol> newController = 2066 [self controllerForNode:newParent]; 2067 [newController addButtonForNode:newNode atIndex:newIndex]; 2068 // If we go from 0 --> 1 bookmarks we may need to hide the 2069 // "bookmarks go here" text container. 2070 [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; 2071 } 2072 2073 // TODO(jrg): for now this is brute force. 2074 - (void)nodeChanged:(BookmarkModel*)model 2075 node:(const BookmarkNode*)node { 2076 [self loaded:model]; 2077 } 2078 2079 - (void)nodeMoved:(BookmarkModel*)model 2080 oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex 2081 newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex { 2082 const BookmarkNode* movedNode = newParent->GetChild(newIndex); 2083 id<BookmarkButtonControllerProtocol> oldController = 2084 [self controllerForNode:oldParent]; 2085 id<BookmarkButtonControllerProtocol> newController = 2086 [self controllerForNode:newParent]; 2087 if (newController == oldController) { 2088 [oldController moveButtonFromIndex:oldIndex toIndex:newIndex]; 2089 } else { 2090 [oldController removeButton:oldIndex animate:NO]; 2091 [newController addButtonForNode:movedNode atIndex:newIndex]; 2092 } 2093 // If the bar is one of the parents we may need to update the visibility 2094 // of the "bookmarks go here" presentation. 2095 [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; 2096 // If we moved the only item on the "off the side" menu somewhere 2097 // else, we may no longer need to show it. 2098 [self configureOffTheSideButtonContentsAndVisibility]; 2099 } 2100 2101 - (void)nodeRemoved:(BookmarkModel*)model 2102 parent:(const BookmarkNode*)oldParent index:(int)index { 2103 // If a context menu is open, close it. 2104 [self cancelMenuTracking]; 2105 2106 // Locate the parent node. The parent may not be showing, in which case 2107 // we do nothing. 2108 id<BookmarkButtonControllerProtocol> parentController = 2109 [self controllerForNode:oldParent]; 2110 [parentController removeButton:index animate:YES]; 2111 // If we go from 1 --> 0 bookmarks we may need to show the 2112 // "bookmarks go here" text container. 2113 [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; 2114 // If we deleted the only item on the "off the side" menu we no 2115 // longer need to show it. 2116 [self configureOffTheSideButtonContentsAndVisibility]; 2117 } 2118 2119 // TODO(jrg): linear searching is bad. 2120 // Need a BookmarkNode-->NSCell mapping. 2121 // 2122 // TODO(jrg): if the bookmark bar is open on launch, we see the 2123 // buttons all placed, then "scooted over" as the favicons load. If 2124 // this looks bad I may need to change widthForBookmarkButtonCell to 2125 // add space for an image even if not there on the assumption that 2126 // favicons will eventually load. 2127 - (void)nodeFaviconLoaded:(BookmarkModel*)model 2128 node:(const BookmarkNode*)node { 2129 for (BookmarkButton* button in buttons_.get()) { 2130 const BookmarkNode* cellnode = [button bookmarkNode]; 2131 if (cellnode == node) { 2132 [[button cell] setBookmarkCellText:[button title] 2133 image:[self faviconForNode:node]]; 2134 // Adding an image means we might need more room for the 2135 // bookmark. Test for it by growing the button (if needed) 2136 // and shifting everything else over. 2137 [self checkForBookmarkButtonGrowth:button]; 2138 } 2139 } 2140 2141 if (folderController_) 2142 [folderController_ faviconLoadedForNode:node]; 2143 } 2144 2145 // TODO(jrg): for now this is brute force. 2146 - (void)nodeChildrenReordered:(BookmarkModel*)model 2147 node:(const BookmarkNode*)node { 2148 [self loaded:model]; 2149 } 2150 2151 #pragma mark BookmarkBarState Protocol 2152 2153 // (BookmarkBarState protocol) 2154 - (BOOL)isVisible { 2155 return barIsEnabled_ && (visualState_ == bookmarks::kShowingState || 2156 visualState_ == bookmarks::kDetachedState || 2157 lastVisualState_ == bookmarks::kShowingState || 2158 lastVisualState_ == bookmarks::kDetachedState); 2159 } 2160 2161 // (BookmarkBarState protocol) 2162 - (BOOL)isAnimationRunning { 2163 return lastVisualState_ != bookmarks::kInvalidState; 2164 } 2165 2166 // (BookmarkBarState protocol) 2167 - (BOOL)isInState:(bookmarks::VisualState)state { 2168 return visualState_ == state && 2169 lastVisualState_ == bookmarks::kInvalidState; 2170 } 2171 2172 // (BookmarkBarState protocol) 2173 - (BOOL)isAnimatingToState:(bookmarks::VisualState)state { 2174 return visualState_ == state && 2175 lastVisualState_ != bookmarks::kInvalidState; 2176 } 2177 2178 // (BookmarkBarState protocol) 2179 - (BOOL)isAnimatingFromState:(bookmarks::VisualState)state { 2180 return lastVisualState_ == state; 2181 } 2182 2183 // (BookmarkBarState protocol) 2184 - (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState 2185 toState:(bookmarks::VisualState)toState { 2186 return lastVisualState_ == fromState && visualState_ == toState; 2187 } 2188 2189 // (BookmarkBarState protocol) 2190 - (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState 2191 andState:(bookmarks::VisualState)toState { 2192 return (lastVisualState_ == fromState && visualState_ == toState) || 2193 (visualState_ == fromState && lastVisualState_ == toState); 2194 } 2195 2196 // (BookmarkBarState protocol) 2197 - (CGFloat)detachedMorphProgress { 2198 if ([self isInState:bookmarks::kDetachedState]) { 2199 return 1; 2200 } 2201 if ([self isAnimatingToState:bookmarks::kDetachedState]) { 2202 return static_cast<CGFloat>( 2203 [[self animatableView] currentAnimationProgress]); 2204 } 2205 if ([self isAnimatingFromState:bookmarks::kDetachedState]) { 2206 return static_cast<CGFloat>( 2207 1 - [[self animatableView] currentAnimationProgress]); 2208 } 2209 return 0; 2210 } 2211 2212 #pragma mark BookmarkBarToolbarViewController Protocol 2213 2214 - (int)currentTabContentsHeight { 2215 TabContents* tc = browser_->GetSelectedTabContents(); 2216 return tc ? tc->view()->GetContainerSize().height() : 0; 2217 } 2218 2219 - (ui::ThemeProvider*)themeProvider { 2220 return ThemeServiceFactory::GetForProfile(browser_->profile()); 2221 } 2222 2223 #pragma mark BookmarkButtonDelegate Protocol 2224 2225 - (void)fillPasteboard:(NSPasteboard*)pboard 2226 forDragOfButton:(BookmarkButton*)button { 2227 [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; 2228 } 2229 2230 // BookmarkButtonDelegate protocol implementation. When menus are 2231 // "active" (e.g. you clicked to open one), moving the mouse over 2232 // another folder button should close the 1st and open the 2nd (like 2233 // real menus). We detect and act here. 2234 - (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { 2235 DCHECK([sender isKindOfClass:[BookmarkButton class]]); 2236 2237 // If folder menus are not being shown, do nothing. This is different from 2238 // BookmarkBarFolderController's implementation because the bar should NOT 2239 // automatically open folder menus when the mouse passes over a folder 2240 // button while the BookmarkBarFolderController DOES automically open 2241 // a subfolder menu. 2242 if (!showFolderMenus_) 2243 return; 2244 2245 // From here down: same logic as BookmarkBarFolderController. 2246 // TODO(jrg): find a way to share these 4 non-comment lines? 2247 // http://crbug.com/35966 2248 // If already opened, then we exited but re-entered the button, so do nothing. 2249 if ([folderController_ parentButton] == sender) 2250 return; 2251 // Else open a new one if it makes sense to do so. 2252 if ([sender bookmarkNode]->is_folder()) { 2253 [folderTarget_ openBookmarkFolderFromButton:sender]; 2254 } else { 2255 // We're over a non-folder bookmark so close any old folders. 2256 [folderController_ close]; 2257 folderController_ = nil; 2258 } 2259 } 2260 2261 // BookmarkButtonDelegate protocol implementation. 2262 - (void)mouseExitedButton:(id)sender event:(NSEvent*)event { 2263 // Don't care; do nothing. 2264 // This is different behavior that the folder menus. 2265 } 2266 2267 - (NSWindow*)browserWindow { 2268 return [[self view] window]; 2269 } 2270 2271 - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { 2272 return [self canEditBookmarks] && 2273 [self canEditBookmark:[button bookmarkNode]]; 2274 } 2275 2276 - (void)didDragBookmarkToTrash:(BookmarkButton*)button { 2277 if ([self canDragBookmarkButtonToTrash:button]) { 2278 const BookmarkNode* node = [button bookmarkNode]; 2279 if (node) { 2280 const BookmarkNode* parent = node->parent(); 2281 bookmarkModel_->Remove(parent, 2282 parent->GetIndexOf(node)); 2283 } 2284 } 2285 } 2286 2287 - (void)bookmarkDragDidEnd:(BookmarkButton*)button 2288 operation:(NSDragOperation)operation { 2289 [self closeFolderAndStopTrackingMenus]; 2290 [button setHidden:NO]; 2291 [self resetAllButtonPositionsWithAnimation:YES]; 2292 } 2293 2294 2295 #pragma mark BookmarkButtonControllerProtocol 2296 2297 // Close all bookmark folders. "Folder" here is the fake menu for 2298 // bookmark folders, not a button context menu. 2299 - (void)closeAllBookmarkFolders { 2300 [self watchForExitEvent:NO]; 2301 [folderController_ close]; 2302 folderController_ = nil; 2303 } 2304 2305 - (void)closeBookmarkFolder:(id)sender { 2306 // We're the top level, so close one means close them all. 2307 [self closeAllBookmarkFolders]; 2308 } 2309 2310 - (BookmarkModel*)bookmarkModel { 2311 return bookmarkModel_; 2312 } 2313 2314 - (BOOL)draggingAllowed:(id<NSDraggingInfo>)info { 2315 return [self canEditBookmarks]; 2316 } 2317 2318 // TODO(jrg): much of this logic is duped with 2319 // [BookmarkBarFolderController draggingEntered:] except when noted. 2320 // http://crbug.com/35966 2321 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { 2322 NSPoint point = [info draggingLocation]; 2323 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; 2324 2325 // Don't allow drops that would result in cycles. 2326 if (button) { 2327 NSData* data = [[info draggingPasteboard] 2328 dataForType:kBookmarkButtonDragType]; 2329 if (data && [info draggingSource]) { 2330 BookmarkButton* sourceButton = nil; 2331 [data getBytes:&sourceButton length:sizeof(sourceButton)]; 2332 const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; 2333 const BookmarkNode* destNode = [button bookmarkNode]; 2334 if (destNode->HasAncestor(sourceNode)) 2335 button = nil; 2336 } 2337 } 2338 2339 if ([button isFolder]) { 2340 if (hoverButton_ == button) { 2341 return NSDragOperationMove; // already open or timed to open 2342 } 2343 if (hoverButton_) { 2344 // Oops, another one triggered or open. 2345 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ 2346 target]]; 2347 // Unlike BookmarkBarFolderController, we do not delay the close 2348 // of the previous one. Given the lack of diagonal movement, 2349 // there is no need, and it feels awkward to do so. See 2350 // comments about kDragHoverCloseDelay in 2351 // bookmark_bar_folder_controller.mm for more details. 2352 [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; 2353 hoverButton_.reset(); 2354 } 2355 hoverButton_.reset([button retain]); 2356 DCHECK([[hoverButton_ target] 2357 respondsToSelector:@selector(openBookmarkFolderFromButton:)]); 2358 [[hoverButton_ target] 2359 performSelector:@selector(openBookmarkFolderFromButton:) 2360 withObject:hoverButton_ 2361 afterDelay:bookmarks::kDragHoverOpenDelay 2362 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; 2363 } 2364 if (!button) { 2365 if (hoverButton_) { 2366 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; 2367 [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; 2368 hoverButton_.reset(); 2369 } 2370 } 2371 2372 // Thrown away but kept to be consistent with the draggingEntered: interface. 2373 return NSDragOperationMove; 2374 } 2375 2376 - (void)draggingExited:(id<NSDraggingInfo>)info { 2377 // NOT the same as a cancel --> we may have moved the mouse into the submenu. 2378 if (hoverButton_) { 2379 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; 2380 hoverButton_.reset(); 2381 } 2382 } 2383 2384 - (BOOL)dragShouldLockBarVisibility { 2385 return ![self isInState:bookmarks::kDetachedState] && 2386 ![self isAnimatingToState:bookmarks::kDetachedState]; 2387 } 2388 2389 // TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController. 2390 // http://crbug.com/35966 2391 - (BOOL)dragButton:(BookmarkButton*)sourceButton 2392 to:(NSPoint)point 2393 copy:(BOOL)copy { 2394 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); 2395 const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; 2396 return [self dragBookmark:sourceNode to:point copy:copy]; 2397 } 2398 2399 - (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { 2400 BOOL dragged = NO; 2401 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]); 2402 if (nodes.size()) { 2403 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); 2404 NSPoint dropPoint = [info draggingLocation]; 2405 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin(); 2406 it != nodes.end(); ++it) { 2407 const BookmarkNode* sourceNode = *it; 2408 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; 2409 } 2410 } 2411 return dragged; 2412 } 2413 2414 - (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { 2415 std::vector<const BookmarkNode*> dragDataNodes; 2416 BookmarkNodeData dragData; 2417 if(dragData.ReadFromDragClipboard()) { 2418 BookmarkModel* bookmarkModel = [self bookmarkModel]; 2419 Profile* profile = bookmarkModel->profile(); 2420 std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile)); 2421 dragDataNodes.assign(nodes.begin(), nodes.end()); 2422 } 2423 return dragDataNodes; 2424 } 2425 2426 // Return YES if we should show the drop indicator, else NO. 2427 - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { 2428 return ![self buttonForDroppingOnAtPoint:point]; 2429 } 2430 2431 // Return the x position for a drop indicator. 2432 - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { 2433 CGFloat x = 0; 2434 int destIndex = [self indexForDragToPoint:point]; 2435 int numButtons = displayedButtonCount_; 2436 2437 // If it's a drop strictly between existing buttons ... 2438 2439 if (destIndex == 0) { 2440 x = 0.5 * bookmarks::kBookmarkHorizontalPadding; 2441 } else if (destIndex > 0 && destIndex < numButtons) { 2442 // ... put the indicator right between the buttons. 2443 BookmarkButton* button = 2444 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex-1)]; 2445 DCHECK(button); 2446 NSRect buttonFrame = [button frame]; 2447 x = NSMaxX(buttonFrame) + 0.5 * bookmarks::kBookmarkHorizontalPadding; 2448 2449 // If it's a drop at the end (past the last button, if there are any) ... 2450 } else if (destIndex == numButtons) { 2451 // and if it's past the last button ... 2452 if (numButtons > 0) { 2453 // ... find the last button, and put the indicator to its right. 2454 BookmarkButton* button = 2455 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; 2456 DCHECK(button); 2457 NSRect buttonFrame = [button frame]; 2458 x = NSMaxX(buttonFrame) + 0.5 * bookmarks::kBookmarkHorizontalPadding; 2459 2460 // Otherwise, put it right at the beginning. 2461 } else { 2462 x = 0.5 * bookmarks::kBookmarkHorizontalPadding; 2463 } 2464 } else { 2465 NOTREACHED(); 2466 } 2467 2468 return x; 2469 } 2470 2471 - (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { 2472 // If the bookmarkbar is not in detached mode, lock bar visibility, forcing 2473 // the overlay to stay open when in fullscreen mode. 2474 if (![self isInState:bookmarks::kDetachedState] && 2475 ![self isAnimatingToState:bookmarks::kDetachedState]) { 2476 BrowserWindowController* browserController = 2477 [BrowserWindowController browserWindowControllerForView:[self view]]; 2478 [browserController lockBarVisibilityForOwner:child 2479 withAnimation:NO 2480 delay:NO]; 2481 } 2482 } 2483 2484 - (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { 2485 // Release bar visibility, allowing the overlay to close if in fullscreen 2486 // mode. 2487 BrowserWindowController* browserController = 2488 [BrowserWindowController browserWindowControllerForView:[self view]]; 2489 [browserController releaseBarVisibilityForOwner:child 2490 withAnimation:NO 2491 delay:NO]; 2492 } 2493 2494 // Add a new folder controller as triggered by the given folder button. 2495 - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { 2496 2497 // If doing a close/open, make sure the fullscreen chrome doesn't 2498 // have a chance to begin animating away in the middle of things. 2499 BrowserWindowController* browserController = 2500 [BrowserWindowController browserWindowControllerForView:[self view]]; 2501 // Confirm we're not re-locking with ourself as an owner before locking. 2502 DCHECK([browserController isBarVisibilityLockedForOwner:self] == NO); 2503 [browserController lockBarVisibilityForOwner:self 2504 withAnimation:NO 2505 delay:NO]; 2506 2507 if (folderController_) 2508 [self closeAllBookmarkFolders]; 2509 2510 // Folder controller, like many window controllers, owns itself. 2511 folderController_ = 2512 [[BookmarkBarFolderController alloc] initWithParentButton:parentButton 2513 parentController:nil 2514 barController:self]; 2515 [folderController_ showWindow:self]; 2516 2517 // Only BookmarkBarController has this; the 2518 // BookmarkBarFolderController does not. 2519 [self watchForExitEvent:YES]; 2520 2521 // No longer need to hold the lock; the folderController_ now owns it. 2522 [browserController releaseBarVisibilityForOwner:self 2523 withAnimation:NO 2524 delay:NO]; 2525 } 2526 2527 - (void)openAll:(const BookmarkNode*)node 2528 disposition:(WindowOpenDisposition)disposition { 2529 [self closeFolderAndStopTrackingMenus]; 2530 bookmark_utils::OpenAll([[self view] window], 2531 browser_->profile(), 2532 browser_, 2533 node, 2534 disposition); 2535 } 2536 2537 - (void)addButtonForNode:(const BookmarkNode*)node 2538 atIndex:(NSInteger)buttonIndex { 2539 int newOffset = 0; 2540 if (buttonIndex == -1) 2541 buttonIndex = [buttons_ count]; // New button goes at the end. 2542 if (buttonIndex <= (NSInteger)[buttons_ count]) { 2543 if (buttonIndex) { 2544 BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1]; 2545 NSRect targetFrame = [targetButton frame]; 2546 newOffset = targetFrame.origin.x + NSWidth(targetFrame) + 2547 bookmarks::kBookmarkHorizontalPadding; 2548 } 2549 BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset]; 2550 ++displayedButtonCount_; 2551 [buttons_ insertObject:newButton atIndex:buttonIndex]; 2552 [buttonView_ addSubview:newButton]; 2553 [self resetAllButtonPositionsWithAnimation:NO]; 2554 // See if any buttons need to be pushed off to or brought in from the side. 2555 [self reconfigureBookmarkBar]; 2556 } else { 2557 // A button from somewhere else (not the bar) is being moved to the 2558 // off-the-side so insure it gets redrawn if its showing. 2559 [self reconfigureBookmarkBar]; 2560 [folderController_ reconfigureMenu]; 2561 } 2562 } 2563 2564 // TODO(mrossetti): Duplicate code with BookmarkBarFolderController. 2565 // http://crbug.com/35966 2566 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { 2567 DCHECK([urls count] == [titles count]); 2568 BOOL nodesWereAdded = NO; 2569 // Figure out where these new bookmarks nodes are to be added. 2570 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; 2571 const BookmarkNode* destParent = NULL; 2572 int destIndex = 0; 2573 if ([button isFolder]) { 2574 destParent = [button bookmarkNode]; 2575 // Drop it at the end. 2576 destIndex = [button bookmarkNode]->child_count(); 2577 } else { 2578 // Else we're dropping somewhere on the bar, so find the right spot. 2579 destParent = bookmarkModel_->GetBookmarkBarNode(); 2580 destIndex = [self indexForDragToPoint:point]; 2581 } 2582 2583 // Don't add the bookmarks if the destination index shows an error. 2584 if (destIndex >= 0) { 2585 // Create and add the new bookmark nodes. 2586 size_t urlCount = [urls count]; 2587 for (size_t i = 0; i < urlCount; ++i) { 2588 GURL gurl; 2589 const char* string = [[urls objectAtIndex:i] UTF8String]; 2590 if (string) 2591 gurl = GURL(string); 2592 // We only expect to receive valid URLs. 2593 DCHECK(gurl.is_valid()); 2594 if (gurl.is_valid()) { 2595 bookmarkModel_->AddURL(destParent, 2596 destIndex++, 2597 base::SysNSStringToUTF16( 2598 [titles objectAtIndex:i]), 2599 gurl); 2600 nodesWereAdded = YES; 2601 } 2602 } 2603 } 2604 return nodesWereAdded; 2605 } 2606 2607 - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { 2608 if (fromIndex != toIndex) { 2609 NSInteger buttonCount = (NSInteger)[buttons_ count]; 2610 if (toIndex == -1) 2611 toIndex = buttonCount; 2612 // See if we have a simple move within the bar, which will be the case if 2613 // both button indexes are in the visible space. 2614 if (fromIndex < buttonCount && toIndex < buttonCount) { 2615 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; 2616 [buttons_ removeObjectAtIndex:fromIndex]; 2617 [buttons_ insertObject:movedButton atIndex:toIndex]; 2618 [movedButton setHidden:NO]; 2619 [self resetAllButtonPositionsWithAnimation:NO]; 2620 } else if (fromIndex < buttonCount) { 2621 // A button is being removed from the bar and added to off-the-side. 2622 // By now the node has already been inserted into the model so the 2623 // button to be added is represented by |toIndex|. Things get 2624 // complicated because the off-the-side is showing and must be redrawn 2625 // while possibly re-laying out the bookmark bar. 2626 [self removeButton:fromIndex animate:NO]; 2627 [self reconfigureBookmarkBar]; 2628 [folderController_ reconfigureMenu]; 2629 } else if (toIndex < buttonCount) { 2630 // A button is being added to the bar and removed from off-the-side. 2631 // By now the node has already been inserted into the model so the 2632 // button to be added is represented by |toIndex|. 2633 const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); 2634 const BookmarkNode* movedNode = node->GetChild(toIndex); 2635 DCHECK(movedNode); 2636 [self addButtonForNode:movedNode atIndex:toIndex]; 2637 [self reconfigureBookmarkBar]; 2638 } else { 2639 // A button is being moved within the off-the-side. 2640 fromIndex -= buttonCount; 2641 toIndex -= buttonCount; 2642 [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex]; 2643 } 2644 } 2645 } 2646 2647 - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { 2648 if (buttonIndex < (NSInteger)[buttons_ count]) { 2649 // The button being removed is showing in the bar. 2650 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; 2651 if (oldButton == [folderController_ parentButton]) { 2652 // If we are deleting a button whose folder is currently open, close it! 2653 [self closeAllBookmarkFolders]; 2654 } 2655 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation]; 2656 [oldButton setDelegate:nil]; 2657 [oldButton removeFromSuperview]; 2658 if (animate && !ignoreAnimations_ && [self isVisible]) 2659 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, 2660 NSZeroSize, nil, nil, nil); 2661 [buttons_ removeObjectAtIndex:buttonIndex]; 2662 --displayedButtonCount_; 2663 [self resetAllButtonPositionsWithAnimation:YES]; 2664 [self reconfigureBookmarkBar]; 2665 } else if (folderController_ && 2666 [folderController_ parentButton] == offTheSideButton_) { 2667 // The button being removed is in the OTS (off-the-side) and the OTS 2668 // menu is showing so we need to remove the button. 2669 NSInteger index = buttonIndex - displayedButtonCount_; 2670 [folderController_ removeButton:index animate:YES]; 2671 } 2672 } 2673 2674 - (id<BookmarkButtonControllerProtocol>)controllerForNode: 2675 (const BookmarkNode*)node { 2676 // See if it's in the bar, then if it is in the hierarchy of visible 2677 // folder menus. 2678 if (bookmarkModel_->GetBookmarkBarNode() == node) 2679 return self; 2680 return [folderController_ controllerForNode:node]; 2681 } 2682 2683 #pragma mark BookmarkButtonControllerProtocol 2684 2685 // NOT an override of a standard Cocoa call made to NSViewControllers. 2686 - (void)hookForEvent:(NSEvent*)theEvent { 2687 if ([self isEventAnExitEvent:theEvent]) 2688 [self closeFolderAndStopTrackingMenus]; 2689 } 2690 2691 #pragma mark TestingAPI Only 2692 2693 - (NSMenu*)buttonContextMenu { 2694 return buttonContextMenu_; 2695 } 2696 2697 // Intentionally ignores ownership issues; used for testing and we try 2698 // to minimize touching the object passed in (likely a mock). 2699 - (void)setButtonContextMenu:(id)menu { 2700 buttonContextMenu_ = menu; 2701 } 2702 2703 - (void)setIgnoreAnimations:(BOOL)ignore { 2704 ignoreAnimations_ = ignore; 2705 } 2706 2707 @end 2708