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