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