// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" #include "base/mac/bundle_locations.h" #include "base/metrics/histogram.h" #include "base/prefs/pref_service.h" #include "base/strings/sys_string_conversions.h" #include "chrome/browser/bookmarks/bookmark_model_factory.h" #include "chrome/browser/bookmarks/bookmark_stats.h" #include "chrome/browser/bookmarks/chrome_bookmark_client.h" #include "chrome/browser/bookmarks/chrome_bookmark_client_factory.h" #include "chrome/browser/prefs/incognito_mode_prefs.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/themes/theme_properties.h" #include "chrome/browser/themes/theme_service.h" #import "chrome/browser/themes/theme_service_factory.h" #include "chrome/browser/ui/bookmarks/bookmark_editor.h" #include "chrome/browser/ui/bookmarks/bookmark_utils.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/chrome_pages.h" #import "chrome/browser/ui/cocoa/background_gradient_view.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_cocoa.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_context_menu_cocoa_controller.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" #import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h" #import "chrome/browser/ui/cocoa/browser_window_controller.h" #import "chrome/browser/ui/cocoa/menu_button.h" #import "chrome/browser/ui/cocoa/presentation_mode_controller.h" #import "chrome/browser/ui/cocoa/themed_window.h" #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" #import "chrome/browser/ui/cocoa/view_id_util.h" #import "chrome/browser/ui/cocoa/view_resizer.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/extensions/extension_constants.h" #include "chrome/common/extensions/extension_metrics.h" #include "chrome/common/pref_names.h" #include "chrome/common/url_constants.h" #include "chrome/grit/generated_resources.h" #include "components/bookmarks/browser/bookmark_model.h" #include "components/bookmarks/browser/bookmark_node_data.h" #include "components/bookmarks/browser/bookmark_utils.h" #include "content/public/browser/user_metrics.h" #include "content/public/browser/web_contents.h" #include "extensions/browser/extension_registry.h" #include "extensions/common/extension.h" #include "extensions/common/extension_set.h" #include "grit/theme_resources.h" #import "ui/base/cocoa/cocoa_base_utils.h" #include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image.h" #include "ui/resources/grit/ui_resources.h" using base::UserMetricsAction; using bookmarks::BookmarkNodeData; using content::OpenURLParams; using content::Referrer; using content::WebContents; // Bookmark bar state changing and animations // // The bookmark bar has three real states: "showing" (a normal bar attached to // the toolbar), "hidden", and "detached" (pretending to be part of the web // content on the NTP). It can, or at least should be able to, animate between // these states. There are several complications even without animation: // - The placement of the bookmark bar is done by the BWC, and it needs to know // the state in order to place the bookmark bar correctly (immediately below // the toolbar when showing, below the infobar when detached). // - The "divider" (a black line) needs to be drawn by either the toolbar (when // the bookmark bar is hidden or detached) or by the bookmark bar (when it is // showing). It should not be drawn by both. // - The toolbar needs to vertically "compress" when the bookmark bar is // showing. This ensures the proper display of both the bookmark bar and the // toolbar, and gives a padded area around the bookmark bar items for right // clicks, etc. // // Our model is that the BWC controls us and also the toolbar. We try not to // talk to the browser nor the toolbar directly, instead centralizing control in // the BWC. The key method by which the BWC controls us is // |-updateState:ChangeType:|. This invokes state changes, and at appropriate // times we request that the BWC do things for us via either the resize delegate // or our general delegate. If the BWC needs any information about what it // should do, or tell the toolbar to do, it can then query us back (e.g., // |-isShownAs...|, |-getDesiredToolbarHeightCompression|, // |-toolbarDividerOpacity|, etc.). // // Animation-related complications: // - Compression of the toolbar is touchy during animation. It must not be // compressed while the bookmark bar is animating to/from showing (from/to // hidden), otherwise it would look like the bookmark bar's contents are // sliding out of the controls inside the toolbar. As such, we have to make // sure that the bookmark bar is shown at the right location and at the // right height (at various points in time). // - Showing the divider is also complicated during animation between hidden // and showing. We have to make sure that the toolbar does not show the // divider despite the fact that it's not compressed. The exception to this // is at the beginning/end of the animation when the toolbar is still // uncompressed but the bookmark bar has height 0. If we're not careful, we // get a flicker at this point. // - We have to ensure that we do the right thing if we're told to change state // while we're running an animation. The generic/easy thing to do is to jump // to the end state of our current animation, and (if the new state change // again involves an animation) begin the new animation. We can do better // than that, however, and sometimes just change the current animation to go // to the new end state (e.g., by "reversing" the animation in the showing -> // hidden -> showing case). We also have to ensure that demands to // immediately change state are always honoured. // // Pointers to animation logic: // - |-moveToState:withAnimation:| starts animations, deciding which ones we // know how to handle. // - |-doBookmarkBarAnimation| has most of the actual logic. // - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain // related logic. // - The BWC's |-layoutSubviews| needs to know how to position things. // - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and // |-bookmarkBar:willAnimateFromState:toState:| in order to inform the // toolbar of required changes. namespace { // Duration of the bookmark bar animations. const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; const NSTimeInterval kDragAndDropAnimationDuration = 0.25; void RecordAppLaunch(Profile* profile, GURL url) { const extensions::Extension* extension = extensions::ExtensionRegistry::Get(profile)-> enabled_extensions().GetAppByURL(url); if (!extension) return; extensions::RecordAppLaunchType(extension_misc::APP_LAUNCH_BOOKMARK_BAR, extension->GetType()); } } // namespace @interface BookmarkBarController(Private) // Moves to the given next state (from the current state), possibly animating. // If |animate| is NO, it will stop any running animation and jump to the given // state. If YES, it may either (depending on implementation) jump to the end of // the current animation and begin the next one, or stop the current animation // mid-flight and animate to the next state. - (void)moveToState:(BookmarkBar::State)nextState withAnimation:(BOOL)animate; // Return the backdrop to the bookmark bar as various types. - (BackgroundGradientView*)backgroundGradientView; - (AnimatableView*)animatableView; // Create buttons for all items in the given bookmark node tree. // Modifies self->buttons_. Do not add more buttons than will fit on the view. - (void)addNodesToButtonList:(const BookmarkNode*)node; // Create an autoreleased button appropriate for insertion into the bookmark // bar. Update |xOffset| with the offset appropriate for the subsequent button. - (BookmarkButton*)buttonForNode:(const BookmarkNode*)node xOffset:(int*)xOffset; // Puts stuff into the final state without animating, stopping a running // animation if necessary. - (void)finalizeState; // Stops any current animation in its tracks (midway). - (void)stopCurrentAnimation; // Show/hide the bookmark bar. // if |animate| is YES, the changes are made using the animator; otherwise they // are made immediately. - (void)showBookmarkBarWithAnimation:(BOOL)animate; // Handles animating the resize of the content view. Returns YES if it handled // the animation, NO if not (and hence it should be done instantly). - (BOOL)doBookmarkBarAnimation; // |point| is in the base coordinate system of the destination window; // it comes from an id. |copy| is YES if a copy is to be // made and inserted into the new location while leaving the bookmark in // the old location, otherwise move the bookmark by removing from its old // location and inserting into the new location. - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode to:(NSPoint)point copy:(BOOL)copy; // Returns the index in the model for a drag to the location given by // |point|. This is determined by finding the first button before the center // of which |point| falls, scanning left to right. Note that, currently, only // the x-coordinate of |point| is considered. Though not currently implemented, // we may check for errors, in which case this would return negative value; // callers should check for this. - (int)indexForDragToPoint:(NSPoint)point; // Add or remove buttons to/from the bar until it is filled but not overflowed. - (void)redistributeButtonsOnBarAsNeeded; // Determine the nature of the bookmark bar contents based on the number of // buttons showing. If too many then show the off-the-side list, if none // then show the no items label. - (void)reconfigureBookmarkBar; - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu; - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu; - (void)tagEmptyMenu:(NSMenu*)menu; - (void)clearMenuTagMap; - (int)preferredHeight; - (void)addButtonsToView; - (BOOL)setManagedBookmarksButtonVisibility; - (BOOL)setOtherBookmarksButtonVisibility; - (BOOL)setAppsPageShortcutButtonVisibility; - (BookmarkButton*)createCustomBookmarkButtonForCell:(NSCell*)cell; - (void)createManagedBookmarksButton; - (void)createOtherBookmarksButton; - (void)createAppsPageShortcutButton; - (void)openAppsPage:(id)sender; - (void)centerNoItemsLabel; - (void)positionRightSideButtons; - (void)watchForExitEvent:(BOOL)watch; - (void)resetAllButtonPositionsWithAnimation:(BOOL)animate; @end @implementation BookmarkBarController @synthesize currentState = currentState_; @synthesize lastState = lastState_; @synthesize isAnimationRunning = isAnimationRunning_; @synthesize delegate = delegate_; @synthesize stateAnimationsEnabled = stateAnimationsEnabled_; @synthesize innerContentAnimationsEnabled = innerContentAnimationsEnabled_; - (id)initWithBrowser:(Browser*)browser initialWidth:(CGFloat)initialWidth delegate:(id)delegate resizeDelegate:(id)resizeDelegate { if ((self = [super initWithNibName:@"BookmarkBar" bundle:base::mac::FrameworkBundle()])) { currentState_ = BookmarkBar::HIDDEN; lastState_ = BookmarkBar::HIDDEN; browser_ = browser; initialWidth_ = initialWidth; bookmarkModel_ = BookmarkModelFactory::GetForProfile(browser_->profile()); bookmarkClient_ = ChromeBookmarkClientFactory::GetForProfile(browser_->profile()); buttons_.reset([[NSMutableArray alloc] init]); delegate_ = delegate; resizeDelegate_ = resizeDelegate; folderTarget_.reset( [[BookmarkFolderTarget alloc] initWithController:self profile:browser_->profile()]); ResourceBundle& rb = ResourceBundle::GetSharedInstance(); folderImage_.reset( rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER).CopyNSImage()); defaultImage_.reset( rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage()); innerContentAnimationsEnabled_ = YES; stateAnimationsEnabled_ = YES; // Register for theme changes, bookmark button pulsing, ... NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; [defaultCenter addObserver:self selector:@selector(themeDidChangeNotification:) name:kBrowserThemeDidChangeNotification object:nil]; [defaultCenter addObserver:self selector:@selector(pulseBookmarkNotification:) name:bookmark_button::kPulseBookmarkButtonNotification object:nil]; contextMenuController_.reset( [[BookmarkContextMenuCocoaController alloc] initWithBookmarkBarController:self]); // This call triggers an -awakeFromNib, which builds the bar, which might // use |folderImage_| and |contextMenuController_|. Ensure it happens after // |folderImage_| is loaded and |contextMenuController_| is created. [[self animatableView] setResizeDelegate:resizeDelegate]; } return self; } - (Browser*)browser { return browser_; } - (BookmarkContextMenuCocoaController*)menuController { return contextMenuController_.get(); } - (void)pulseBookmarkNotification:(NSNotification*)notification { NSDictionary* dict = [notification userInfo]; const BookmarkNode* node = NULL; NSValue *value = [dict objectForKey:bookmark_button::kBookmarkKey]; DCHECK(value); if (value) node = static_cast([value pointerValue]); NSNumber* number = [dict objectForKey:bookmark_button::kBookmarkPulseFlagKey]; DCHECK(number); BOOL doPulse = number ? [number boolValue] : NO; // 3 cases: // button on the bar: flash it // button in "other bookmarks" folder: flash other bookmarks // button in "off the side" folder: flash the chevron for (BookmarkButton* button in [self buttons]) { if ([button bookmarkNode] == node) { [button setIsContinuousPulsing:doPulse]; return; } } if ([managedBookmarksButton_ bookmarkNode] == node) { [managedBookmarksButton_ setIsContinuousPulsing:doPulse]; return; } if ([otherBookmarksButton_ bookmarkNode] == node) { [otherBookmarksButton_ setIsContinuousPulsing:doPulse]; return; } if (node->parent() == bookmarkModel_->bookmark_bar_node()) { [offTheSideButton_ setIsContinuousPulsing:doPulse]; return; } NOTREACHED() << "no bookmark button found to pulse!"; } - (void)dealloc { // Clear delegate so it doesn't get called during stopAnimation. [[self animatableView] setResizeDelegate:nil]; // We better stop any in-flight animation if we're being killed. [[self animatableView] stopAnimation]; // Remove our view from its superview so it doesn't attempt to reference // it when the controller is gone. //TODO(dmaclach): Remove -- http://crbug.com/25845 [[self view] removeFromSuperview]; // Be sure there is no dangling pointer. if ([[self view] respondsToSelector:@selector(setController:)]) [[self view] performSelector:@selector(setController:) withObject:nil]; // For safety, make sure the buttons can no longer call us. for (BookmarkButton* button in buttons_.get()) { [button setDelegate:nil]; [button setTarget:nil]; [button setAction:nil]; } bridge_.reset(NULL); [[NSNotificationCenter defaultCenter] removeObserver:self]; [self watchForExitEvent:NO]; [super dealloc]; } - (void)awakeFromNib { // We default to NOT open, which means height=0. DCHECK([[self view] isHidden]); // Hidden so it's OK to change. // Set our initial height to zero, since that is what the superview // expects. We will resize ourselves open later if needed. [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)]; // Complete init of the "off the side" button, as much as we can. ResourceBundle& rb = ResourceBundle::GetSharedInstance(); [offTheSideButton_ setImage: rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_CHEVRONS).ToNSImage()]; [offTheSideButton_.draggableButton setDraggable:NO]; [offTheSideButton_.draggableButton setActsOnMouseDown:YES]; // We are enabled by default. barIsEnabled_ = YES; // Remember the original sizes of the 'no items' and 'import bookmarks' // fields to aid in resizing when the window frame changes. originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame]; originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame]; // To make life happier when the bookmark bar is floating, the chevron is a // child of the button view. [offTheSideButton_ removeFromSuperview]; [buttonView_ addSubview:offTheSideButton_]; // When resized we may need to add new buttons, or remove them (if // no longer visible), or add/remove the "off the side" menu. [[self view] setPostsFrameChangedNotifications:YES]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameDidChange) name:NSViewFrameDidChangeNotification object:[self view]]; // Watch for things going to or from fullscreen. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterOrLeaveFullscreen:) name:kWillEnterFullscreenNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterOrLeaveFullscreen:) name:kWillLeaveFullscreenNotification object:nil]; // Don't pass ourself along (as 'self') until our init is completely // done. Thus, this call is (almost) last. bridge_.reset(new BookmarkBarBridge(browser_->profile(), self, bookmarkModel_)); } // Called by our main view (a BookmarkBarView) when it gets moved to a // window. We perform operations which need to know the relevant // window (e.g. watch for a window close) so they can't be performed // earlier (such as in awakeFromNib). - (void)viewDidMoveToWindow { NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; // Remove any existing notifications before registering for new ones. [defaultCenter removeObserver:self name:NSWindowWillCloseNotification object:nil]; [defaultCenter removeObserver:self name:NSWindowDidResignMainNotification object:nil]; [defaultCenter addObserver:self selector:@selector(parentWindowWillClose:) name:NSWindowWillCloseNotification object:[[self view] window]]; [defaultCenter addObserver:self selector:@selector(parentWindowDidResignMain:) name:NSWindowDidResignMainNotification object:[[self view] window]]; } // When going fullscreen we can run into trouble. Our view is removed // from the non-fullscreen window before the non-fullscreen window // loses key, so our parentDidResignKey: callback never gets called. // In addition, a bookmark folder controller needs to be autoreleased // (in case it's in the event chain when closed), but the release // implicitly needs to happen while it's connected to the original // (non-fullscreen) window to "unlock bar visibility". Such a // contract isn't honored when going fullscreen with the menu option // (not with the keyboard shortcut). We fake it as best we can here. // We have a similar problem leaving fullscreen. - (void)willEnterOrLeaveFullscreen:(NSNotification*)notification { if (folderController_) { [self childFolderWillClose:folderController_]; [self closeFolderAndStopTrackingMenus]; } } // NSNotificationCenter callback. - (void)parentWindowWillClose:(NSNotification*)notification { [self closeFolderAndStopTrackingMenus]; } // NSNotificationCenter callback. - (void)parentWindowDidResignMain:(NSNotification*)notification { [self closeFolderAndStopTrackingMenus]; } // Change the layout of the bookmark bar's subviews in response to a visibility // change (e.g., show or hide the bar) or style change (attached or floating). - (void)layoutSubviews { NSRect frame = [[self view] frame]; NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame)); // Add padding to the detached bookmark bar. // The state of our morph (if any); 1 is total bubble, 0 is the regular bar. CGFloat morph = [self detachedMorphProgress]; CGFloat padding = bookmarks::kNTPBookmarkBarPadding; buttonViewFrame = NSInsetRect(buttonViewFrame, morph * padding, morph * padding); [buttonView_ setFrame:buttonViewFrame]; // Update bookmark button backgrounds. if ([self isAnimationRunning]) { for (NSButton* button in buttons_.get()) [button setNeedsDisplay:YES]; // Update the apps and other buttons explicitly, since they are not in the // buttons_ array. [appsPageShortcutButton_ setNeedsDisplay:YES]; [managedBookmarksButton_ setNeedsDisplay:YES]; [otherBookmarksButton_ setNeedsDisplay:YES]; } } // We don't change a preference; we only change visibility. Preference changing // (global state) is handled in |chrome::ToggleBookmarkBarWhenVisible()|. We // simply update based on what we're told. - (void)updateVisibility { [self showBookmarkBarWithAnimation:NO]; } - (void)updateExtraButtonsVisibility { if (!appsPageShortcutButton_.get() || !managedBookmarksButton_.get()) return; [self setAppsPageShortcutButtonVisibility]; [self setManagedBookmarksButtonVisibility]; [self resetAllButtonPositionsWithAnimation:NO]; [self reconfigureBookmarkBar]; } - (void)updateHiddenState { BOOL oldHidden = [[self view] isHidden]; BOOL newHidden = ![self isVisible]; if (oldHidden != newHidden) [[self view] setHidden:newHidden]; } - (void)setBookmarkBarEnabled:(BOOL)enabled { if (enabled != barIsEnabled_) { barIsEnabled_ = enabled; [self updateVisibility]; } } - (CGFloat)getDesiredToolbarHeightCompression { // Some special cases.... if (!barIsEnabled_) return 0; if ([self isAnimationRunning]) { // No toolbar compression when animating between hidden and showing, nor // between showing and detached. if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN andState:BookmarkBar::SHOW] || [self isAnimatingBetweenState:BookmarkBar::SHOW andState:BookmarkBar::DETACHED]) return 0; // If we ever need any other animation cases, code would go here. } return [self isInState:BookmarkBar::SHOW] ? bookmarks::kBookmarkBarOverlap : 0; } - (CGFloat)toolbarDividerOpacity { // Some special cases.... if ([self isAnimationRunning]) { // In general, the toolbar shouldn't show a divider while we're animating // between showing and hidden. The exception is when our height is < 1, in // which case we can't draw it. It's all-or-nothing (no partial opacity). if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN andState:BookmarkBar::SHOW]) return (NSHeight([[self view] frame]) < 1) ? 1 : 0; // The toolbar should show the divider when animating between showing and // detached (but opacity will vary). if ([self isAnimatingBetweenState:BookmarkBar::SHOW andState:BookmarkBar::DETACHED]) return static_cast([self detachedMorphProgress]); // If we ever need any other animation cases, code would go here. } // In general, only show the divider when it's in the normal showing state. return [self isInState:BookmarkBar::SHOW] ? 0 : 1; } - (NSImage*)faviconForNode:(const BookmarkNode*)node { if (!node) return defaultImage_; if (node == bookmarkClient_->managed_node()) { // Most users never see this node, so the image is only loaded if needed. ResourceBundle& rb = ResourceBundle::GetSharedInstance(); return rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER_MANAGED).ToNSImage(); } if (node->is_folder()) return folderImage_; const gfx::Image& favicon = bookmarkModel_->GetFavicon(node); if (!favicon.IsEmpty()) return favicon.ToNSImage(); return defaultImage_; } - (void)closeFolderAndStopTrackingMenus { showFolderMenus_ = NO; [self closeAllBookmarkFolders]; } - (BOOL)canEditBookmarks { PrefService* prefs = browser_->profile()->GetPrefs(); return prefs->GetBoolean(bookmarks::prefs::kEditBookmarksEnabled); } - (BOOL)canEditBookmark:(const BookmarkNode*)node { // Don't allow edit/delete of the permanent nodes. if (node == nil || bookmarkModel_->is_permanent_node(node) || !bookmarkClient_->CanBeEditedByUser(node)) { return NO; } return YES; } #pragma mark Actions // Helper methods called on the main thread by runMenuFlashThread. - (void)setButtonFlashStateOn:(id)sender { [sender highlight:YES]; } - (void)setButtonFlashStateOff:(id)sender { [sender highlight:NO]; } - (void)cleanupAfterMenuFlashThread:(id)sender { [self closeFolderAndStopTrackingMenus]; // Items retained by doMenuFlashOnSeparateThread below. [sender release]; [self release]; } // End runMenuFlashThread helper methods. // This call is invoked only by doMenuFlashOnSeparateThread below. // It makes the selected BookmarkButton (which is masquerading as a menu item) // flash a few times to give confirmation feedback, then it closes the menu. // It spends all its time sleeping or scheduling UI work on the main thread. - (void)runMenuFlashThread:(id)sender { // Check this is not running on the main thread, as it sleeps. DCHECK(![NSThread isMainThread]); // Duration of flash phases and number of flashes designed to evoke a // slightly retro "more mac-like than the Mac" feel. // Current Cocoa UI has a barely perceptible flash,probably because Apple // doesn't fire the action til after the animation and so there's a hurry. // As this code is fully asynchronous, it can take its time. const float kBBOnFlashTime = 0.08; const float kBBOffFlashTime = 0.08; const int kBookmarkButtonMenuFlashes = 3; for (int count = 0 ; count < kBookmarkButtonMenuFlashes ; count++) { [self performSelectorOnMainThread:@selector(setButtonFlashStateOn:) withObject:sender waitUntilDone:NO]; [NSThread sleepForTimeInterval:kBBOnFlashTime]; [self performSelectorOnMainThread:@selector(setButtonFlashStateOff:) withObject:sender waitUntilDone:NO]; [NSThread sleepForTimeInterval:kBBOffFlashTime]; } [self performSelectorOnMainThread:@selector(cleanupAfterMenuFlashThread:) withObject:sender waitUntilDone:NO]; } // Non-blocking call which starts the process to make the selected menu item // flash a few times to give confirmation feedback, after which it closes the // menu. The item is of course actually a BookmarkButton masquerading as a menu // item). - (void)doMenuFlashOnSeparateThread:(id)sender { // Ensure that self and sender don't go away before the animation completes. // These retains are balanced in cleanupAfterMenuFlashThread above. [self retain]; [sender retain]; [NSThread detachNewThreadSelector:@selector(runMenuFlashThread:) toTarget:self withObject:sender]; } - (IBAction)openBookmark:(id)sender { BOOL isMenuItem = [[sender cell] isFolderButtonCell]; BOOL animate = isMenuItem && innerContentAnimationsEnabled_; if (animate) [self doMenuFlashOnSeparateThread:sender]; DCHECK([sender respondsToSelector:@selector(bookmarkNode)]); const BookmarkNode* node = [sender bookmarkNode]; DCHECK(node); WindowOpenDisposition disposition = ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); RecordAppLaunch(browser_->profile(), node->url()); [self openURL:node->url() disposition:disposition]; if (!animate) [self closeFolderAndStopTrackingMenus]; RecordBookmarkLaunch(node, [self bookmarkLaunchLocation]); } // Common function to open a bookmark folder of any type. - (void)openBookmarkFolder:(id)sender { DCHECK([sender isKindOfClass:[BookmarkButton class]]); DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]); // Only record the action if it's the initial folder being opened. if (!showFolderMenus_) RecordBookmarkFolderOpen([self bookmarkLaunchLocation]); showFolderMenus_ = !showFolderMenus_; // Middle click on chevron should not open bookmarks under it, instead just // open its folder menu. if (sender == offTheSideButton_) { [[sender cell] setStartingChildIndex:displayedButtonCount_]; NSEvent* event = [NSApp currentEvent]; if ([event type] == NSOtherMouseUp) { [self openOrCloseBookmarkFolderForOffTheSideButton]; return; } } // Toggle presentation of bar folder menus. [folderTarget_ openBookmarkFolderFromButton:sender]; } - (void)openOrCloseBookmarkFolderForOffTheSideButton { // If clicked on already opened folder, then close it and return. if ([folderController_ parentButton] == offTheSideButton_) [self closeBookmarkFolder:self]; else [self addNewFolderControllerWithParentButton:offTheSideButton_]; } // Click on a bookmark folder button. - (IBAction)openBookmarkFolderFromButton:(id)sender { [self openBookmarkFolder:sender]; } // Click on the "off the side" button (chevron), which opens like a folder // button but isn't exactly a parent folder. - (IBAction)openOffTheSideFolderFromButton:(id)sender { [self openBookmarkFolder:sender]; } - (IBAction)importBookmarks:(id)sender { chrome::ShowImportDialog(browser_); } #pragma mark Private Methods // Called after a theme change took place, possibly for a different profile. - (void)themeDidChangeNotification:(NSNotification*)notification { [self updateTheme:[[[self view] window] themeProvider]]; } // (Private) Method is the same as [self view], but is provided to be explicit. - (BackgroundGradientView*)backgroundGradientView { DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]); return (BackgroundGradientView*)[self view]; } // (Private) Method is the same as [self view], but is provided to be explicit. - (AnimatableView*)animatableView { DCHECK([[self view] isKindOfClass:[AnimatableView class]]); return (AnimatableView*)[self view]; } - (BookmarkLaunchLocation)bookmarkLaunchLocation { return currentState_ == BookmarkBar::DETACHED ? BOOKMARK_LAUNCH_LOCATION_DETACHED_BAR : BOOKMARK_LAUNCH_LOCATION_ATTACHED_BAR; } // Position the right-side buttons including the off-the-side chevron. - (void)positionRightSideButtons { int maxX = NSMaxX([[self buttonView] bounds]) - bookmarks::kBookmarkHorizontalPadding; int right = maxX; int ignored = 0; NSRect frame = [self frameForBookmarkButtonFromCell: [otherBookmarksButton_ cell] xOffset:&ignored]; if (![otherBookmarksButton_ isHidden]) { right -= NSWidth(frame); frame.origin.x = right; } else { frame.origin.x = maxX - NSWidth(frame); } [otherBookmarksButton_ setFrame:frame]; frame = [offTheSideButton_ frame]; frame.size.height = bookmarks::kBookmarkFolderButtonHeight; right -= frame.size.width; frame.origin.x = right; [offTheSideButton_ setFrame:frame]; } // Configure the off-the-side button (e.g. specify the node range, // check if we should enable or disable it, etc). - (void)configureOffTheSideButtonContentsAndVisibility { [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_]; [[offTheSideButton_ cell] setBookmarkNode:bookmarkModel_->bookmark_bar_node()]; int bookmarkChildren = bookmarkModel_->bookmark_bar_node()->child_count(); if (bookmarkChildren > displayedButtonCount_) { [offTheSideButton_ setHidden:NO]; } else { // If we just deleted the last item in an off-the-side menu so the // button will be going away, make sure the menu goes away. if (folderController_ && ([folderController_ parentButton] == offTheSideButton_)) [self closeAllBookmarkFolders]; // (And hide the button, too.) [offTheSideButton_ setHidden:YES]; } } // Main menubar observation code, so we can know to close our fake menus if the // user clicks on the actual menubar, as multiple unconnected menus sharing // the screen looks weird. // Needed because the local event monitor doesn't see the click on the menubar. // Gets called when the menubar is clicked. - (void)begunTracking:(NSNotification *)notification { [self closeFolderAndStopTrackingMenus]; } // Install the callback. - (void)startObservingMenubar { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(begunTracking:) name:NSMenuDidBeginTrackingNotification object:[NSApp mainMenu]]; } // Remove the callback. - (void)stopObservingMenubar { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self name:NSMenuDidBeginTrackingNotification object:[NSApp mainMenu]]; } // End of menubar observation code. // Begin (or end) watching for a click outside this window. Unlike // normal NSWindows, bookmark folder "fake menu" windows do not become // key or main. Thus, traditional notification (e.g. WillResignKey) // won't work. Our strategy is to watch (at the app level) for a // "click outside" these windows to detect when they logically lose // focus. - (void)watchForExitEvent:(BOOL)watch { if (watch) { if (!exitEventTap_) { exitEventTap_ = [NSEvent addLocalMonitorForEventsMatchingMask:NSAnyEventMask handler:^NSEvent* (NSEvent* event) { if ([self isEventAnExitEvent:event]) [self closeFolderAndStopTrackingMenus]; return event; }]; [self startObservingMenubar]; } } else { if (exitEventTap_) { [NSEvent removeMonitor:exitEventTap_]; exitEventTap_ = nil; [self stopObservingMenubar]; } } } // Keep the "no items" label centered in response to a frame size change. - (void)centerNoItemsLabel { // Note that this computation is done in the parent's coordinate system, // which is unflipped. Also, we want the label to be a fixed distance from // the bottom, so that it slides up properly (on animating to hidden). // The textfield sits in the itemcontainer, so to center it we maintain // equal vertical padding on the top and bottom. int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) - NSHeight([[buttonView_ noItemContainer] frame])) / 2; [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)]; } // (Private) - (void)showBookmarkBarWithAnimation:(BOOL)animate { if (animate && stateAnimationsEnabled_) { // If |-doBookmarkBarAnimation| does the animation, we're done. if ([self doBookmarkBarAnimation]) return; // Else fall through and do the change instantly. } // Set our height. [resizeDelegate_ resizeView:[self view] newHeight:[self preferredHeight]]; // Only show the divider if showing the normal bookmark bar. BOOL showsDivider = [self isInState:BookmarkBar::SHOW]; [[self backgroundGradientView] setShowsDivider:showsDivider]; // Make sure we're shown. [[self view] setHidden:![self isVisible]]; // Update everything else. [self layoutSubviews]; [self frameDidChange]; } // (Private) - (BOOL)doBookmarkBarAnimation { if ([self isAnimatingFromState:BookmarkBar::HIDDEN toState:BookmarkBar::SHOW]) { [[self backgroundGradientView] setShowsDivider:YES]; [[self view] setHidden:NO]; AnimatableView* view = [self animatableView]; // Height takes into account the extra height we have since the toolbar // only compresses when we're done. [view animateToNewHeight:(chrome::kBookmarkBarHeight - bookmarks::kBookmarkBarOverlap) duration:kBookmarkBarAnimationDuration]; } else if ([self isAnimatingFromState:BookmarkBar::SHOW toState:BookmarkBar::HIDDEN]) { [[self backgroundGradientView] setShowsDivider:YES]; [[self view] setHidden:NO]; AnimatableView* view = [self animatableView]; [view animateToNewHeight:0 duration:kBookmarkBarAnimationDuration]; } else if ([self isAnimatingFromState:BookmarkBar::SHOW toState:BookmarkBar::DETACHED]) { [[self backgroundGradientView] setShowsDivider:YES]; [[self view] setHidden:NO]; AnimatableView* view = [self animatableView]; [view animateToNewHeight:chrome::kNTPBookmarkBarHeight duration:kBookmarkBarAnimationDuration]; } else if ([self isAnimatingFromState:BookmarkBar::DETACHED toState:BookmarkBar::SHOW]) { [[self backgroundGradientView] setShowsDivider:YES]; [[self view] setHidden:NO]; AnimatableView* view = [self animatableView]; // Height takes into account the extra height we have since the toolbar // only compresses when we're done. [view animateToNewHeight:(chrome::kBookmarkBarHeight - bookmarks::kBookmarkBarOverlap) duration:kBookmarkBarAnimationDuration]; } else { // Oops! An animation we don't know how to handle. return NO; } return YES; } // Actually open the URL. This is the last chance for a unit test to // override. - (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition { OpenURLParams params( url, Referrer(), disposition, ui::PAGE_TRANSITION_AUTO_BOOKMARK, false); browser_->OpenURL(params); } - (void)clearMenuTagMap { seedId_ = 0; menuTagMap_.clear(); } - (int)preferredHeight { DCHECK(![self isAnimationRunning]); if (!barIsEnabled_) return 0; switch (currentState_) { case BookmarkBar::SHOW: return chrome::kBookmarkBarHeight; case BookmarkBar::DETACHED: return chrome::kNTPBookmarkBarHeight; case BookmarkBar::HIDDEN: return 0; } } // Recursively add the given bookmark node and all its children to // menu, one menu item per node. - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu { NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child]; NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""] autorelease]; [menu addItem:item]; [item setImage:[self faviconForNode:child]]; if (child->is_folder()) { NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease]; [menu setSubmenu:submenu forItem:item]; if (!child->empty()) { [self addFolderNode:child toMenu:submenu]; // potentially recursive } else { [self tagEmptyMenu:submenu]; } } else { [item setTarget:self]; [item setAction:@selector(openBookmarkMenuItem:)]; [item setTag:[self menuTagFromNodeId:child->id()]]; if (child->is_url()) [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:child]]; } } // Empty menus are odd; if empty, add something to look at. // Matches windows behavior. - (void)tagEmptyMenu:(NSMenu*)menu { NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU); [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title action:NULL keyEquivalent:@""] autorelease]]; } // Add the children of the given bookmark node (and their children...) // to menu, one menu item per node. - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu { for (int i = 0; i < node->child_count(); i++) { const BookmarkNode* child = node->GetChild(i); [self addNode:child toMenu:menu]; } } // Return an autoreleased NSMenu that represents the given bookmark // folder node. - (NSMenu *)menuForFolderNode:(const BookmarkNode*)node { if (!node->is_folder()) return nil; NSString* title = base::SysUTF16ToNSString(node->GetTitle()); NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease]; [self addFolderNode:node toMenu:menu]; if (![menu numberOfItems]) { [self tagEmptyMenu:menu]; } return menu; } // Return an appropriate width for the given bookmark button cell. // The "+2" is needed because, sometimes, Cocoa is off by a tad. // Example: for a bookmark named "Moma" or "SFGate", it is one pixel // too small. For "FBL" it is 2 pixels too small. // For a bookmark named "SFGateFooWoo", it is just fine. - (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell { CGFloat desired = [cell cellSize].width + 2; return std::min(desired, bookmarks::kDefaultBookmarkWidth); } - (IBAction)openBookmarkMenuItem:(id)sender { int64 tag = [self nodeIdFromMenuTag:[sender tag]]; const BookmarkNode* node = bookmarks::GetBookmarkNodeByID(bookmarkModel_, tag); WindowOpenDisposition disposition = ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); [self openURL:node->url() disposition:disposition]; } // For the given root node of the bookmark bar, show or hide (as // appropriate) the "no items" container (text which says "bookmarks // go here"). - (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node { BOOL hideNoItemWarning = !node->empty(); [[buttonView_ noItemContainer] setHidden:hideNoItemWarning]; } // TODO(jrg): write a "build bar" so there is a nice spot for things // like the contextual menu which is invoked when not over a // bookmark. On Safari that menu has a "new folder" option. - (void)addNodesToButtonList:(const BookmarkNode*)node { [self showOrHideNoItemContainerForNode:node]; CGFloat maxViewX = NSMaxX([[self view] bounds]); int xOffset = bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding; // Draw the apps bookmark if needed. if (![appsPageShortcutButton_ isHidden]) { NSRect frame = [self frameForBookmarkButtonFromCell:[appsPageShortcutButton_ cell] xOffset:&xOffset]; [appsPageShortcutButton_ setFrame:frame]; } // Draw the managed bookmark folder if needed. if (![managedBookmarksButton_ isHidden]) { xOffset += bookmarks::kBookmarkHorizontalPadding; NSRect frame = [self frameForBookmarkButtonFromCell:[managedBookmarksButton_ cell] xOffset:&xOffset]; [managedBookmarksButton_ setFrame:frame]; } for (int i = 0; i < node->child_count(); i++) { const BookmarkNode* child = node->GetChild(i); BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; if (NSMinX([button frame]) >= maxViewX) { [button setDelegate:nil]; break; } [buttons_ addObject:button]; } } - (BookmarkButton*)buttonForNode:(const BookmarkNode*)node xOffset:(int*)xOffset { BookmarkButtonCell* cell = [self cellForBookmarkNode:node]; NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset]; base::scoped_nsobject button( [[BookmarkButton alloc] initWithFrame:frame]); DCHECK(button.get()); // [NSButton setCell:] warns to NOT use setCell: other than in the // initializer of a control. However, we are using a basic // NSButton whose initializer does not take an NSCell as an // object. To honor the assumed semantics, we do nothing with // NSButton between alloc/init and setCell:. [button setCell:cell]; [button setDelegate:self]; // We cannot set the button cell's text color until it is placed in // the button (e.g. the [button setCell:cell] call right above). We // also cannot set the cell's text color until the view is added to // the hierarchy. If that second part is now true, set the color. // (If not we'll set the color on the 1st themeChanged: // notification.) ui::ThemeProvider* themeProvider = [[[self view] window] themeProvider]; if (themeProvider) { NSColor* color = themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT); [cell setTextColor:color]; } if (node->is_folder()) { [button setTarget:self]; [button setAction:@selector(openBookmarkFolderFromButton:)]; [[button draggableButton] setActsOnMouseDown:YES]; // If it has a title, and it will be truncated, show full title in // tooltip. NSString* title = base::SysUTF16ToNSString(node->GetTitle()); if ([title length] && [[button cell] cellSize].width > bookmarks::kDefaultBookmarkWidth) { [button setToolTip:title]; } } else { // Make the button do something [button setTarget:self]; [button setAction:@selector(openBookmark:)]; if (node->is_url()) [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]]; } return [[button.get() retain] autorelease]; } // Add bookmark buttons to the view only if they are completely // visible and don't overlap the "other bookmarks". Remove buttons // which are clipped. Called when building the bookmark bar the first time. - (void)addButtonsToView { displayedButtonCount_ = 0; NSMutableArray* buttons = [self buttons]; for (NSButton* button in buttons) { if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) - bookmarks::kBookmarkHorizontalPadding)) break; [buttonView_ addSubview:button]; ++displayedButtonCount_; } NSUInteger removalCount = [buttons count] - (NSUInteger)displayedButtonCount_; if (removalCount > 0) { NSRange removalRange = NSMakeRange(displayedButtonCount_, removalCount); [buttons removeObjectsInRange:removalRange]; } } // Shows or hides the Other Bookmarks button as appropriate, and returns // whether it ended up visible. - (BOOL)setManagedBookmarksButtonVisibility { if (!managedBookmarksButton_.get()) return NO; PrefService* prefs = browser_->profile()->GetPrefs(); BOOL visible = ![managedBookmarksButton_ bookmarkNode]->empty() && prefs->GetBoolean(bookmarks::prefs::kShowManagedBookmarksInBookmarkBar); BOOL currentVisibility = ![managedBookmarksButton_ isHidden]; if (currentVisibility != visible) { [managedBookmarksButton_ setHidden:!visible]; [self resetAllButtonPositionsWithAnimation:NO]; } return visible; } // Shows or hides the Other Bookmarks button as appropriate, and returns // whether it ended up visible. - (BOOL)setOtherBookmarksButtonVisibility { if (!otherBookmarksButton_.get()) return NO; BOOL visible = ![otherBookmarksButton_ bookmarkNode]->empty(); [otherBookmarksButton_ setHidden:!visible]; return visible; } // Shows or hides the Apps button as appropriate, and returns whether it ended // up visible. - (BOOL)setAppsPageShortcutButtonVisibility { if (!appsPageShortcutButton_.get()) return NO; BOOL visible = bookmarkModel_->loaded() && chrome::ShouldShowAppsShortcutInBookmarkBar( browser_->profile(), browser_->host_desktop_type()); [appsPageShortcutButton_ setHidden:!visible]; return visible; } // Creates a bookmark bar button that does not correspond to a regular bookmark // or folder. It is used by the "Other Bookmarks" and the "Apps" buttons. - (BookmarkButton*)createCustomBookmarkButtonForCell:(NSCell*)cell { BookmarkButton* button = [[BookmarkButton alloc] init]; [[button draggableButton] setDraggable:NO]; [[button draggableButton] setActsOnMouseDown:YES]; [button setCell:cell]; [button setDelegate:self]; [button setTarget:self]; // Make sure this button, like all other BookmarkButtons, lives // until the end of the current event loop. [[button retain] autorelease]; return button; } // Creates the button for "Managed Bookmarks", but does not position it. - (void)createManagedBookmarksButton { if (managedBookmarksButton_.get()) { // The node's title might have changed if the user signed in or out. // Make sure it's up to date now. const BookmarkNode* node = bookmarkClient_->managed_node(); NSString* title = base::SysUTF16ToNSString(node->GetTitle()); NSCell* cell = [managedBookmarksButton_ cell]; [cell setTitle:title]; // Its visibility may have changed too. [self setManagedBookmarksButtonVisibility]; return; } NSCell* cell = [self cellForBookmarkNode:bookmarkClient_->managed_node()]; managedBookmarksButton_.reset([self createCustomBookmarkButtonForCell:cell]); [managedBookmarksButton_ setAction:@selector(openBookmarkFolderFromButton:)]; view_id_util::SetID(managedBookmarksButton_.get(), VIEW_ID_MANAGED_BOOKMARKS); [buttonView_ addSubview:managedBookmarksButton_.get()]; [self setManagedBookmarksButtonVisibility]; } // Creates the button for "Other Bookmarks", but does not position it. - (void)createOtherBookmarksButton { // Can't create this until the model is loaded, but only need to // create it once. if (otherBookmarksButton_.get()) { [self setOtherBookmarksButtonVisibility]; return; } NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()]; otherBookmarksButton_.reset([self createCustomBookmarkButtonForCell:cell]); // Peg at right; keep same height as bar. [otherBookmarksButton_ setAutoresizingMask:(NSViewMinXMargin)]; [otherBookmarksButton_ setAction:@selector(openBookmarkFolderFromButton:)]; view_id_util::SetID(otherBookmarksButton_.get(), VIEW_ID_OTHER_BOOKMARKS); [buttonView_ addSubview:otherBookmarksButton_.get()]; [self setOtherBookmarksButtonVisibility]; } // Creates the button for "Apps", but does not position it. - (void)createAppsPageShortcutButton { // Can't create this until the model is loaded, but only need to // create it once. if (appsPageShortcutButton_.get()) { [self setAppsPageShortcutButtonVisibility]; return; } ResourceBundle& rb = ResourceBundle::GetSharedInstance(); NSString* text = l10n_util::GetNSString(IDS_BOOKMARK_BAR_APPS_SHORTCUT_NAME); NSImage* image = rb.GetNativeImageNamed( IDR_BOOKMARK_BAR_APPS_SHORTCUT).ToNSImage(); NSCell* cell = [self cellForCustomButtonWithText:text image:image]; appsPageShortcutButton_.reset([self createCustomBookmarkButtonForCell:cell]); [[appsPageShortcutButton_ draggableButton] setActsOnMouseDown:NO]; [appsPageShortcutButton_ setAction:@selector(openAppsPage:)]; NSString* tooltip = l10n_util::GetNSString(IDS_BOOKMARK_BAR_APPS_SHORTCUT_TOOLTIP); [appsPageShortcutButton_ setToolTip:tooltip]; [buttonView_ addSubview:appsPageShortcutButton_.get()]; [self setAppsPageShortcutButtonVisibility]; } - (void)openAppsPage:(id)sender { WindowOpenDisposition disposition = ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); [self openURL:GURL(chrome::kChromeUIAppsURL) disposition:disposition]; RecordBookmarkAppsPageOpen([self bookmarkLaunchLocation]); } // To avoid problems with sync, changes that may impact the current // bookmark (e.g. deletion) make sure context menus are closed. This // prevents deleting a node which no longer exists. - (void)cancelMenuTracking { [contextMenuController_ cancelTracking]; } - (void)moveToState:(BookmarkBar::State)nextState withAnimation:(BOOL)animate { BOOL isAnimationRunning = [self isAnimationRunning]; // No-op if the next state is the same as the "current" one, subject to the // following conditions: // - no animation is running; or // - an animation is running and |animate| is YES ([*] if it's NO, we'd want // to cancel the animation and jump to the final state). if ((nextState == currentState_) && (!isAnimationRunning || animate)) return; // If an animation is running, we want to finalize it. Otherwise we'd have to // be able to animate starting from the middle of one type of animation. We // assume that animations that we know about can be "reversed". if (isAnimationRunning) { // Don't cancel if we're going to reverse the animation. if (nextState != lastState_) { [self stopCurrentAnimation]; [self finalizeState]; } // If we're in case [*] above, we can stop here. if (nextState == currentState_) return; } // Now update with the new state change. lastState_ = currentState_; currentState_ = nextState; isAnimationRunning_ = YES; // Animate only if told to and if bar is enabled. if (animate && stateAnimationsEnabled_ && barIsEnabled_) { [self closeAllBookmarkFolders]; // Take care of any animation cases we know how to handle. // We know how to handle hidden <-> normal, normal <-> detached.... if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN andState:BookmarkBar::SHOW] || [self isAnimatingBetweenState:BookmarkBar::SHOW andState:BookmarkBar::DETACHED]) { [delegate_ bookmarkBar:self willAnimateFromState:lastState_ toState:currentState_]; [self showBookmarkBarWithAnimation:YES]; return; } // If we ever need any other animation cases, code would go here. // Let any animation cases which we don't know how to handle fall through to // the unanimated case. } // Just jump to the state. [self finalizeState]; } // N.B.: |-moveToState:...| will check if this should be a no-op or not. - (void)updateState:(BookmarkBar::State)newState changeType:(BookmarkBar::AnimateChangeType)changeType { BOOL animate = changeType == BookmarkBar::ANIMATE_STATE_CHANGE && stateAnimationsEnabled_; [self moveToState:newState withAnimation:animate]; } // (Private) - (void)finalizeState { // We promise that our delegate that the variables will be finalized before // the call to |-bookmarkBar:didChangeFromState:toState:|. BookmarkBar::State oldState = lastState_; lastState_ = currentState_; isAnimationRunning_ = NO; // Notify our delegate. [delegate_ bookmarkBar:self didChangeFromState:oldState toState:currentState_]; // Update ourselves visually. [self updateVisibility]; } // (Private) - (void)stopCurrentAnimation { [[self animatableView] stopAnimation]; } // Delegate method for |AnimatableView| (a superclass of // |BookmarkBarToolbarView|). - (void)animationDidEnd:(NSAnimation*)animation { [self finalizeState]; } - (void)reconfigureBookmarkBar { [self setManagedBookmarksButtonVisibility]; [self redistributeButtonsOnBarAsNeeded]; [self positionRightSideButtons]; [self configureOffTheSideButtonContentsAndVisibility]; [self centerNoItemsLabel]; } // Determine if the given |view| can completely fit within the constraint of // maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum // width. If the minimum width is not achievable then hide the view. Return YES // if the view was hidden. - (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX { BOOL wasHidden = NO; // See if the view needs to be narrowed. NSRect frame = [view frame]; if (NSMaxX(frame) > maxViewX) { // Resize if more than 30 pixels are showing, otherwise hide. if (NSMinX(frame) + 30.0 < maxViewX) { frame.size.width = maxViewX - NSMinX(frame); [view setFrame:frame]; } else { [view setHidden:YES]; wasHidden = YES; } } return wasHidden; } // Bookmark button menu items that open a new window (e.g., open in new window, // open in incognito, edit, etc.) cause us to lose a mouse-exited event // on the button, which leaves it in a hover state. // Since the showsBorderOnlyWhileMouseInside uses a tracking area, simple // tricks (e.g. sending an extra mouseExited: to the button) don't // fix the problem. // http://crbug.com/129338 - (void)unhighlightBookmark:(const BookmarkNode*)node { // Only relevant if context menu was opened from a button on the // bookmark bar. const BookmarkNode* parent = node->parent(); BookmarkNode::Type parentType = parent->type(); if (parentType == BookmarkNode::BOOKMARK_BAR) { int index = parent->GetIndexOf(node); if ((index >= 0) && (static_cast(index) < [buttons_ count])) { NSButton* button = [buttons_ objectAtIndex:static_cast(index)]; if ([button showsBorderOnlyWhileMouseInside]) { [button setShowsBorderOnlyWhileMouseInside:NO]; [button setShowsBorderOnlyWhileMouseInside:YES]; } } } } // Adjust the horizontal width, x position and the visibility of the "For quick // access" text field and "Import bookmarks..." button based on the current // width of the containing |buttonView_| (which is affected by window width). - (void)adjustNoItemContainerForMaxX:(CGFloat)maxViewX { if (![[buttonView_ noItemContainer] isHidden]) { // Reset initial frames for the two items, then adjust as necessary. NSTextField* noItemTextfield = [buttonView_ noItemTextfield]; NSRect noItemsRect = originalNoItemsRect_; NSRect importBookmarksRect = originalImportBookmarksRect_; if (![appsPageShortcutButton_ isHidden]) { float width = NSWidth([appsPageShortcutButton_ frame]); noItemsRect.origin.x += width; importBookmarksRect.origin.x += width; } if (![managedBookmarksButton_ isHidden]) { float width = NSWidth([managedBookmarksButton_ frame]); noItemsRect.origin.x += width; importBookmarksRect.origin.x += width; } [noItemTextfield setFrame:noItemsRect]; [noItemTextfield setHidden:NO]; NSButton* importBookmarksButton = [buttonView_ importBookmarksButton]; [importBookmarksButton setFrame:importBookmarksRect]; [importBookmarksButton setHidden:NO]; // Check each to see if they need to be shrunk or hidden. if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX]) [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX]; } } // Scans through all buttons from left to right, calculating from scratch where // they should be based on the preceding widths, until it finds the one // requested. // Returns NSZeroRect if there is no such button in the bookmark bar. // Enables you to work out where a button will end up when it is done animating. - (NSRect)finalRectOfButton:(BookmarkButton*)wantedButton { CGFloat left = bookmarks::kBookmarkLeftMargin; NSRect buttonFrame = NSZeroRect; // Draw the apps bookmark if needed. if (![appsPageShortcutButton_ isHidden]) { left = NSMaxX([appsPageShortcutButton_ frame]) + bookmarks::kBookmarkHorizontalPadding; } // Draw the managed bookmarks folder if needed. if (![managedBookmarksButton_ isHidden]) { left = NSMaxX([managedBookmarksButton_ frame]) + bookmarks::kBookmarkHorizontalPadding; } for (NSButton* button in buttons_.get()) { // Hidden buttons get no space. if ([button isHidden]) continue; buttonFrame = [button frame]; buttonFrame.origin.x = left; left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding; if (button == wantedButton) return buttonFrame; } return NSZeroRect; } // Calculates the final position of the last button in the bar. // We can't just use [[self buttons] lastObject] frame] because the button // may be animating currently. - (NSRect)finalRectOfLastButton { return [self finalRectOfButton:[[self buttons] lastObject]]; } - (CGFloat)buttonViewMaxXWithOffTheSideButtonIsVisible:(BOOL)visible { CGFloat maxViewX = NSMaxX([buttonView_ bounds]); // If necessary, pull in the width to account for the Other Bookmarks button. if ([self setOtherBookmarksButtonVisibility]) { maxViewX = [otherBookmarksButton_ frame].origin.x - bookmarks::kBookmarkRightMargin; } [self positionRightSideButtons]; // If we're already overflowing, then we need to account for the chevron. if (visible) { maxViewX = [offTheSideButton_ frame].origin.x - bookmarks::kBookmarkRightMargin; } return maxViewX; } - (void)redistributeButtonsOnBarAsNeeded { const BookmarkNode* node = bookmarkModel_->bookmark_bar_node(); NSInteger barCount = node->child_count(); // Determine the current maximum extent of the visible buttons. [self positionRightSideButtons]; BOOL offTheSideButtonVisible = (barCount > displayedButtonCount_); CGFloat maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible: offTheSideButtonVisible]; // As a result of pasting or dragging, the bar may now have more buttons // than will fit so remove any which overflow. They will be shown in // the off-the-side folder. while (displayedButtonCount_ > 0) { BookmarkButton* button = [buttons_ lastObject]; if (NSMaxX([self finalRectOfLastButton]) < maxViewX) break; [buttons_ removeLastObject]; [button setDelegate:nil]; [button removeFromSuperview]; --displayedButtonCount_; // Account for the fact that the chevron might now be visible. if (!offTheSideButtonVisible) { offTheSideButtonVisible = YES; maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:YES]; } } // As a result of cutting, deleting and dragging, the bar may now have room // for more buttons. int xOffset; if (displayedButtonCount_ > 0) { xOffset = NSMaxX([self finalRectOfLastButton]) + bookmarks::kBookmarkHorizontalPadding; } else if (![managedBookmarksButton_ isHidden]) { xOffset = NSMaxX([managedBookmarksButton_ frame]) + bookmarks::kBookmarkHorizontalPadding; } else if (![appsPageShortcutButton_ isHidden]) { xOffset = NSMaxX([appsPageShortcutButton_ frame]) + bookmarks::kBookmarkHorizontalPadding; } else { xOffset = bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding; } for (int i = displayedButtonCount_; i < barCount; ++i) { const BookmarkNode* child = node->GetChild(i); BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; // If we're testing against the last possible button then account // for the chevron no longer needing to be shown. if (i == barCount - 1) maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:NO]; if (NSMaxX([button frame]) > maxViewX) { [button setDelegate:nil]; break; } ++displayedButtonCount_; [buttons_ addObject:button]; [buttonView_ addSubview:button]; } // While we're here, adjust the horizontal width and the visibility // of the "For quick access" and "Import bookmarks..." text fields. if (![buttons_ count]) [self adjustNoItemContainerForMaxX:maxViewX]; } #pragma mark Private Methods Exposed for Testing - (BookmarkBarView*)buttonView { return buttonView_; } - (NSMutableArray*)buttons { return buttons_.get(); } - (NSButton*)offTheSideButton { return offTheSideButton_; } - (NSButton*)appsPageShortcutButton { return appsPageShortcutButton_; } - (BOOL)offTheSideButtonIsHidden { return [offTheSideButton_ isHidden]; } - (BOOL)appsPageShortcutButtonIsHidden { return [appsPageShortcutButton_ isHidden]; } - (BookmarkButton*)otherBookmarksButton { return otherBookmarksButton_.get(); } - (BookmarkBarFolderController*)folderController { return folderController_; } - (id)folderTarget { return folderTarget_.get(); } - (int)displayedButtonCount { return displayedButtonCount_; } // Delete all buttons (bookmarks, chevron, "other bookmarks") from the // bookmark bar; reset knowledge of bookmarks. - (void)clearBookmarkBar { for (BookmarkButton* button in buttons_.get()) { [button setDelegate:nil]; [button removeFromSuperview]; } [buttons_ removeAllObjects]; [self clearMenuTagMap]; displayedButtonCount_ = 0; // Make sure there are no stale pointers in the pasteboard. This // can be important if a bookmark is deleted (via bookmark sync) // while in the middle of a drag. The "drag completed" code // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is // careful enough to bail if there is no data found at "drop" time. // // Unfortunately the clearContents selector is 10.6 only. The best // we can do is make sure something else is present in place of the // stale bookmark. NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self]; [pboard setString:@"" forType:NSStringPboardType]; } // Return an autoreleased NSCell suitable for a bookmark button. // TODO(jrg): move much of the cell config into the BookmarkButtonCell class. - (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node { NSImage* image = node ? [self faviconForNode:node] : nil; BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node text:nil image:image menuController:contextMenuController_]; [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; // Note: a quirk of setting a cell's text color is that it won't work // until the cell is associated with a button, so we can't theme the cell yet. return cell; } // Return an autoreleased NSCell suitable for a special button displayed on the // bookmark bar that is not attached to any bookmark node. // TODO(jrg): move much of the cell config into the BookmarkButtonCell class. - (BookmarkButtonCell*)cellForCustomButtonWithText:(NSString*)text image:(NSImage*)image { BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellWithText:text image:image menuController:contextMenuController_]; [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; // Note: a quirk of setting a cell's text color is that it won't work // until the cell is associated with a button, so we can't theme the cell yet. return cell; } // Returns a frame appropriate for the given bookmark cell, suitable // for creating an NSButton that will contain it. |xOffset| is the X // offset for the frame; it is increased to be an appropriate X offset // for the next button. - (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell xOffset:(int*)xOffset { DCHECK(xOffset); NSRect bounds = [buttonView_ bounds]; bounds.size.height = bookmarks::kBookmarkButtonHeight; NSRect frame = NSInsetRect(bounds, bookmarks::kBookmarkHorizontalPadding, bookmarks::kBookmarkVerticalPadding); frame.size.width = [self widthForBookmarkButtonCell:cell]; // Add an X offset based on what we've already done frame.origin.x += *xOffset; // And up the X offset for next time. *xOffset = NSMaxX(frame); return frame; } // A bookmark button's contents changed. Check for growth // (e.g. increase the width up to the maximum). If we grew, move // other bookmark buttons over. - (void)checkForBookmarkButtonGrowth:(NSButton*)changedButton { NSRect frame = [changedButton frame]; CGFloat desiredSize = [self widthForBookmarkButtonCell:[changedButton cell]]; CGFloat delta = desiredSize - frame.size.width; if (delta) { frame.size.width = desiredSize; [changedButton setFrame:frame]; for (NSButton* button in buttons_.get()) { NSRect buttonFrame = [button frame]; if (buttonFrame.origin.x > frame.origin.x) { buttonFrame.origin.x += delta; [button setFrame:buttonFrame]; } } } // We may have just crossed a threshold to enable the off-the-side // button. [self configureOffTheSideButtonContentsAndVisibility]; } // Called when our controlled frame has changed size. - (void)frameDidChange { if (!bookmarkModel_->loaded()) return; [self updateTheme:[[[self view] window] themeProvider]]; [self reconfigureBookmarkBar]; } // Given a NSMenuItem tag, return the appropriate bookmark node id. - (int64)nodeIdFromMenuTag:(int32)tag { return menuTagMap_[tag]; } // Create and return a new tag for the given node id. - (int32)menuTagFromNodeId:(int64)menuid { int tag = seedId_++; menuTagMap_[tag] = menuid; return tag; } // Adapt appearance of buttons to the current theme. Called after // theme changes, or when our view is added to the view hierarchy. // Oddly, the view pings us instead of us pinging our view. This is // because our trigger is an [NSView viewWillMoveToWindow:], which the // controller doesn't normally know about. Otherwise we don't have // access to the theme before we know what window we will be on. - (void)updateTheme:(ui::ThemeProvider*)themeProvider { if (!themeProvider) return; NSColor* color = themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT); for (BookmarkButton* button in buttons_.get()) { BookmarkButtonCell* cell = [button cell]; [cell setTextColor:color]; } [[managedBookmarksButton_ cell] setTextColor:color]; [[otherBookmarksButton_ cell] setTextColor:color]; [[appsPageShortcutButton_ cell] setTextColor:color]; } // Return YES if the event indicates an exit from the bookmark bar // folder menus. E.g. "click outside" of the area we are watching. // At this time we are watching the area that includes all popup // bookmark folder windows. - (BOOL)isEventAnExitEvent:(NSEvent*)event { NSWindow* eventWindow = [event window]; NSWindow* myWindow = [[self view] window]; switch ([event type]) { case NSLeftMouseDown: case NSRightMouseDown: // If the click is in my window but NOT in the bookmark bar, consider // it a click 'outside'. Clicks directly on an active button (i.e. one // that is a folder and for which its folder menu is showing) are 'in'. // All other clicks on the bookmarks bar are counted as 'outside' // because they should close any open bookmark folder menu. if (eventWindow == myWindow) { NSView* hitView = [[eventWindow contentView] hitTest:[event locationInWindow]]; if (hitView == [folderController_ parentButton]) return NO; if (![hitView isDescendantOf:[self view]] || hitView == buttonView_) return YES; } // If a click in a bookmark bar folder window and that isn't // one of my bookmark bar folders, YES is click outside. if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow class]]) { return YES; } break; case NSKeyDown: { // Event hooks often see the same keydown event twice due to the way key // events get dispatched and redispatched, so ignore if this keydown // event has the EXACT same timestamp as the previous keydown. static NSTimeInterval lastKeyDownEventTime; NSTimeInterval thisTime = [event timestamp]; if (lastKeyDownEventTime != thisTime) { lastKeyDownEventTime = thisTime; if ([event modifierFlags] & NSCommandKeyMask) return YES; else if (folderController_) return [folderController_ handleInputText:[event characters]]; } return NO; } case NSKeyUp: return NO; case NSLeftMouseDragged: // We can get here with the following sequence: // - open a bookmark folder // - right-click (and unclick) on it to open context menu // - move mouse to window titlebar then click-drag it by the titlebar // http://crbug.com/49333 return NO; default: break; } return NO; } #pragma mark Drag & Drop // Find something like std::is_between? I can't believe one doesn't exist. static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { return ((value >= low) && (value <= high)); } // Return the proposed drop target for a hover open button from the // given array, or nil if none. We use this for distinguishing // between a hover-open candidate or drop-indicator draw. // Helper for buttonForDroppingOnAtPoint:. // Get UI review on "middle half" ness. // http://crbug.com/36276 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point fromArray:(NSArray*)array { for (BookmarkButton* button in array) { // Hidden buttons can overlap valid visible buttons, just ignore. if ([button isHidden]) continue; // Break early if we've gone too far. if ((NSMinX([button frame]) > point.x) || (![button superview])) return nil; // Careful -- this only applies to the bar with horiz buttons. // Intentionally NOT using NSPointInRect() so that scrolling into // a submenu doesn't cause it to be closed. if (ValueInRangeInclusive(NSMinX([button frame]), point.x, NSMaxX([button frame]))) { // Over a button but let's be a little more specific (make sure // it's over the middle half, not just over it). NSRect frame = [button frame]; NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0); if (ValueInRangeInclusive(NSMinX(middleHalfOfButton), point.x, NSMaxX(middleHalfOfButton))) { // It makes no sense to drop on a non-folder; there is no hover. if (![button isFolder]) return nil; // Got it! return button; } else { // Over a button but not over the middle half. return nil; } } } // Not hovering over a button. return nil; } // Return the proposed drop target for a hover open button, or nil if // none. Works with both the bookmark buttons and the "Other // Bookmarks" button. Point is in [self view] coordinates. - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { point = [[self view] convertPoint:point fromView:[[[self view] window] contentView]]; // If there's a hover button, return it if the point is within its bounds. // Since the logic in -buttonForDroppingOnAtPoint:fromArray: only matches a // button when the point is over the middle half, this is needed to prevent // the button's folder being closed if the mouse temporarily leaves the // middle half but is still within the button bounds. if (hoverButton_ && NSPointInRect(point, [hoverButton_ frame])) return hoverButton_.get(); BookmarkButton* button = [self buttonForDroppingOnAtPoint:point fromArray:buttons_.get()]; // One more chance -- try "Other Bookmarks" and "off the side" (if visible). // This is different than BookmarkBarFolderController. if (!button) { NSMutableArray* array = [NSMutableArray array]; if (![self offTheSideButtonIsHidden]) [array addObject:offTheSideButton_]; [array addObject:otherBookmarksButton_]; button = [self buttonForDroppingOnAtPoint:point fromArray:array]; } return button; } - (int)indexForDragToPoint:(NSPoint)point { // TODO(jrg): revisit position info based on UI team feedback. // dropLocation is in bar local coordinates. NSPoint dropLocation = [[self view] convertPoint:point fromView:[[[self view] window] contentView]]; BookmarkButton* buttonToTheRightOfDraggedButton = nil; for (BookmarkButton* button in buttons_.get()) { CGFloat midpoint = NSMidX([button frame]); if (dropLocation.x <= midpoint) { buttonToTheRightOfDraggedButton = button; break; } } if (buttonToTheRightOfDraggedButton) { const BookmarkNode* afterNode = [buttonToTheRightOfDraggedButton bookmarkNode]; DCHECK(afterNode); int index = afterNode->parent()->GetIndexOf(afterNode); // Make sure we don't get confused by buttons which aren't visible. return std::min(index, displayedButtonCount_); } // If nothing is to my right I am at the end! return displayedButtonCount_; } // TODO(mrossetti,jrg): Yet more duplicated code. // http://crbug.com/35966 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode to:(NSPoint)point copy:(BOOL)copy { DCHECK(sourceNode); // Drop destination. const BookmarkNode* destParent = NULL; int destIndex = 0; // First check if we're dropping on a button. If we have one, and // it's a folder, drop in it. BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; if ([button isFolder]) { destParent = [button bookmarkNode]; // Drop it at the end. destIndex = [button bookmarkNode]->child_count(); } else { // Else we're dropping somewhere on the bar, so find the right spot. destParent = bookmarkModel_->bookmark_bar_node(); destIndex = [self indexForDragToPoint:point]; } if (!bookmarkClient_->CanBeEditedByUser(destParent)) return NO; if (!bookmarkClient_->CanBeEditedByUser(sourceNode)) copy = YES; // Be sure we don't try and drop a folder into itself. if (sourceNode != destParent) { if (copy) bookmarkModel_->Copy(sourceNode, destParent, destIndex); else bookmarkModel_->Move(sourceNode, destParent, destIndex); } [self closeFolderAndStopTrackingMenus]; // Movement of a node triggers observers (like us) to rebuild the // bar so we don't have to do so explicitly. return YES; } - (void)draggingEnded:(id)info { [self closeFolderAndStopTrackingMenus]; [[BookmarkButton draggedButton] setHidden:NO]; [self resetAllButtonPositionsWithAnimation:YES]; } // Set insertionPos_ and hasInsertionPos_, and make insertion space for a // hypothetical drop with the new button having a left edge of |where|. // Gets called only by our view. - (void)setDropInsertionPos:(CGFloat)where { if (!hasInsertionPos_ || where != insertionPos_) { insertionPos_ = where; hasInsertionPos_ = YES; CGFloat left; if (![managedBookmarksButton_ isHidden]) { left = NSMaxX([managedBookmarksButton_ frame]) + bookmarks::kBookmarkHorizontalPadding; } else if (![appsPageShortcutButton_ isHidden]) { left = NSMaxX([appsPageShortcutButton_ frame]) + bookmarks::kBookmarkHorizontalPadding; } else { left = bookmarks::kBookmarkLeftMargin; } CGFloat paddingWidth = bookmarks::kDefaultBookmarkWidth; BookmarkButton* draggedButton = [BookmarkButton draggedButton]; if (draggedButton) { paddingWidth = std::min(bookmarks::kDefaultBookmarkWidth, NSWidth([draggedButton frame])); } // Put all the buttons where they belong, with all buttons to the right // of the insertion point shuffling right to make space for it. [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:kDragAndDropAnimationDuration]; for (NSButton* button in buttons_.get()) { // Hidden buttons get no space. if ([button isHidden]) continue; NSRect buttonFrame = [button frame]; buttonFrame.origin.x = left; // Update "left" for next time around. left += buttonFrame.size.width; if (left > insertionPos_) buttonFrame.origin.x += paddingWidth; left += bookmarks::kBookmarkHorizontalPadding; if (innerContentAnimationsEnabled_) [[button animator] setFrame:buttonFrame]; else [button setFrame:buttonFrame]; } [NSAnimationContext endGrouping]; } } // Put all visible bookmark bar buttons in their normal locations, either with // or without animation according to the |animate| flag. // This is generally useful, so is called from various places internally. - (void)resetAllButtonPositionsWithAnimation:(BOOL)animate { // Position the apps bookmark if needed. CGFloat left = bookmarks::kBookmarkLeftMargin; if (![appsPageShortcutButton_ isHidden]) { int xOffset = bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding; NSRect frame = [self frameForBookmarkButtonFromCell:[appsPageShortcutButton_ cell] xOffset:&xOffset]; [appsPageShortcutButton_ setFrame:frame]; left = xOffset + bookmarks::kBookmarkHorizontalPadding; } // Position the managed bookmarks folder if needed. if (![managedBookmarksButton_ isHidden]) { int xOffset = left; NSRect frame = [self frameForBookmarkButtonFromCell:[managedBookmarksButton_ cell] xOffset:&xOffset]; [managedBookmarksButton_ setFrame:frame]; left = xOffset + bookmarks::kBookmarkHorizontalPadding; } animate &= innerContentAnimationsEnabled_; for (NSButton* button in buttons_.get()) { // Hidden buttons get no space. if ([button isHidden]) continue; NSRect buttonFrame = [button frame]; buttonFrame.origin.x = left; left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding; if (animate) [[button animator] setFrame:buttonFrame]; else [button setFrame:buttonFrame]; } } // Clear insertion flag, remove insertion space and put all visible bookmark // bar buttons in their normal locations. // Gets called only by our view. - (void)clearDropInsertionPos { if (hasInsertionPos_) { hasInsertionPos_ = NO; [self resetAllButtonPositionsWithAnimation:YES]; } } #pragma mark Bridge Notification Handlers // TODO(jrg): for now this is brute force. - (void)loaded:(BookmarkModel*)model { DCHECK(model == bookmarkModel_); if (!model->loaded()) return; // If this is a rebuild request while we have a folder open, close it. // TODO(mrossetti): Eliminate the need for this because it causes the folder // menu to disappear after a cut/copy/paste/delete change. // See: http://crbug.com/36614 if (folderController_) [self closeAllBookmarkFolders]; // Brute force nuke and build. savedFrameWidth_ = NSWidth([[self view] frame]); const BookmarkNode* node = model->bookmark_bar_node(); [self clearBookmarkBar]; [self createAppsPageShortcutButton]; [self createManagedBookmarksButton]; [self addNodesToButtonList:node]; [self createOtherBookmarksButton]; [self updateTheme:[[[self view] window] themeProvider]]; [self positionRightSideButtons]; [self addButtonsToView]; [self configureOffTheSideButtonContentsAndVisibility]; [self reconfigureBookmarkBar]; } - (void)beingDeleted:(BookmarkModel*)model { // The browser may be being torn down; little is safe to do. As an // example, it may not be safe to clear the pasteboard. // http://crbug.com/38665 } - (void)nodeAdded:(BookmarkModel*)model parent:(const BookmarkNode*)newParent index:(int)newIndex { // If a context menu is open, close it. [self cancelMenuTracking]; const BookmarkNode* newNode = newParent->GetChild(newIndex); id newController = [self controllerForNode:newParent]; [newController addButtonForNode:newNode atIndex:newIndex]; // If we go from 0 --> 1 bookmarks we may need to hide the // "bookmarks go here" text container. [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()]; // Cope with chevron or "Other Bookmarks" buttons possibly changing state. [self reconfigureBookmarkBar]; } // TODO(jrg): for now this is brute force. - (void)nodeChanged:(BookmarkModel*)model node:(const BookmarkNode*)node { [self loaded:model]; } - (void)nodeMoved:(BookmarkModel*)model oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex { const BookmarkNode* movedNode = newParent->GetChild(newIndex); id oldController = [self controllerForNode:oldParent]; id newController = [self controllerForNode:newParent]; if (newController == oldController) { [oldController moveButtonFromIndex:oldIndex toIndex:newIndex]; } else { [oldController removeButton:oldIndex animate:NO]; [newController addButtonForNode:movedNode atIndex:newIndex]; } // If the bar is one of the parents we may need to update the visibility // of the "bookmarks go here" presentation. [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()]; // Cope with chevron or "Other Bookmarks" buttons possibly changing state. [self reconfigureBookmarkBar]; } - (void)nodeRemoved:(BookmarkModel*)model parent:(const BookmarkNode*)oldParent index:(int)index { // If a context menu is open, close it. [self cancelMenuTracking]; // Locate the parent node. The parent may not be showing, in which case // we do nothing. id parentController = [self controllerForNode:oldParent]; [parentController removeButton:index animate:YES]; // If we go from 1 --> 0 bookmarks we may need to show the // "bookmarks go here" text container. [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()]; // If we deleted the only item on the "off the side" menu we no // longer need to show it. [self reconfigureBookmarkBar]; } // TODO(jrg): linear searching is bad. // Need a BookmarkNode-->NSCell mapping. // // TODO(jrg): if the bookmark bar is open on launch, we see the // buttons all placed, then "scooted over" as the favicons load. If // this looks bad I may need to change widthForBookmarkButtonCell to // add space for an image even if not there on the assumption that // favicons will eventually load. - (void)nodeFaviconLoaded:(BookmarkModel*)model node:(const BookmarkNode*)node { for (BookmarkButton* button in buttons_.get()) { const BookmarkNode* cellnode = [button bookmarkNode]; if (cellnode == node) { [[button cell] setBookmarkCellText:[button title] image:[self faviconForNode:node]]; // Adding an image means we might need more room for the // bookmark. Test for it by growing the button (if needed) // and shifting everything else over. [self checkForBookmarkButtonGrowth:button]; return; } } if (folderController_) [folderController_ faviconLoadedForNode:node]; } // TODO(jrg): for now this is brute force. - (void)nodeChildrenReordered:(BookmarkModel*)model node:(const BookmarkNode*)node { [self loaded:model]; } #pragma mark BookmarkBarState Protocol // (BookmarkBarState protocol) - (BOOL)isVisible { return barIsEnabled_ && (currentState_ == BookmarkBar::SHOW || currentState_ == BookmarkBar::DETACHED || lastState_ == BookmarkBar::SHOW || lastState_ == BookmarkBar::DETACHED); } // (BookmarkBarState protocol) - (BOOL)isInState:(BookmarkBar::State)state { return currentState_ == state && ![self isAnimationRunning]; } // (BookmarkBarState protocol) - (BOOL)isAnimatingToState:(BookmarkBar::State)state { return currentState_ == state && [self isAnimationRunning]; } // (BookmarkBarState protocol) - (BOOL)isAnimatingFromState:(BookmarkBar::State)state { return lastState_ == state && [self isAnimationRunning]; } // (BookmarkBarState protocol) - (BOOL)isAnimatingFromState:(BookmarkBar::State)fromState toState:(BookmarkBar::State)toState { return lastState_ == fromState && currentState_ == toState && [self isAnimationRunning]; } // (BookmarkBarState protocol) - (BOOL)isAnimatingBetweenState:(BookmarkBar::State)fromState andState:(BookmarkBar::State)toState { return [self isAnimatingFromState:fromState toState:toState] || [self isAnimatingFromState:toState toState:fromState]; } // (BookmarkBarState protocol) - (CGFloat)detachedMorphProgress { if ([self isInState:BookmarkBar::DETACHED]) { return 1; } if ([self isAnimatingToState:BookmarkBar::DETACHED]) { return static_cast( [[self animatableView] currentAnimationProgress]); } if ([self isAnimatingFromState:BookmarkBar::DETACHED]) { return static_cast( 1 - [[self animatableView] currentAnimationProgress]); } return 0; } #pragma mark BookmarkBarToolbarViewController Protocol - (int)currentTabContentsHeight { BrowserWindowController* browserController = [BrowserWindowController browserWindowControllerForView:[self view]]; return NSHeight([[browserController tabContentArea] frame]); } - (ThemeService*)themeService { return ThemeServiceFactory::GetForProfile(browser_->profile()); } #pragma mark BookmarkButtonDelegate Protocol - (void)fillPasteboard:(NSPasteboard*)pboard forDragOfButton:(BookmarkButton*)button { [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; } // BookmarkButtonDelegate protocol implementation. When menus are // "active" (e.g. you clicked to open one), moving the mouse over // another folder button should close the 1st and open the 2nd (like // real menus). We detect and act here. - (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { DCHECK([sender isKindOfClass:[BookmarkButton class]]); // If folder menus are not being shown, do nothing. This is different from // BookmarkBarFolderController's implementation because the bar should NOT // automatically open folder menus when the mouse passes over a folder // button while the BookmarkBarFolderController DOES automatically open // a subfolder menu. if (!showFolderMenus_) return; // From here down: same logic as BookmarkBarFolderController. // TODO(jrg): find a way to share these 4 non-comment lines? // http://crbug.com/35966 // If already opened, then we exited but re-entered the button, so do nothing. if ([folderController_ parentButton] == sender) return; // Else open a new one if it makes sense to do so. const BookmarkNode* node = [sender bookmarkNode]; if (node && node->is_folder()) { // Update |hoverButton_| so that it corresponds to the open folder. hoverButton_.reset([sender retain]); [folderTarget_ openBookmarkFolderFromButton:sender]; } else { // We're over a non-folder bookmark so close any old folders. [folderController_ close]; folderController_ = nil; } } // BookmarkButtonDelegate protocol implementation. - (void)mouseExitedButton:(id)sender event:(NSEvent*)event { // Don't care; do nothing. // This is different behavior that the folder menus. } - (NSWindow*)browserWindow { return [[self view] window]; } - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { return [self canEditBookmarks] && [self canEditBookmark:[button bookmarkNode]]; } - (void)didDragBookmarkToTrash:(BookmarkButton*)button { if ([self canDragBookmarkButtonToTrash:button]) { const BookmarkNode* node = [button bookmarkNode]; if (node) { const BookmarkNode* parent = node->parent(); bookmarkModel_->Remove(parent, parent->GetIndexOf(node)); } } } - (void)bookmarkDragDidEnd:(BookmarkButton*)button operation:(NSDragOperation)operation { [button setHidden:NO]; [self resetAllButtonPositionsWithAnimation:YES]; } #pragma mark BookmarkButtonControllerProtocol // Close all bookmark folders. "Folder" here is the fake menu for // bookmark folders, not a button context menu. - (void)closeAllBookmarkFolders { [self watchForExitEvent:NO]; [folderController_ close]; folderController_ = nil; } - (void)closeBookmarkFolder:(id)sender { // We're the top level, so close one means close them all. [self closeAllBookmarkFolders]; } - (BookmarkModel*)bookmarkModel { return bookmarkModel_; } - (BOOL)draggingAllowed:(id)info { return [self canEditBookmarks]; } // TODO(jrg): much of this logic is duped with // [BookmarkBarFolderController draggingEntered:] except when noted. // http://crbug.com/35966 - (NSDragOperation)draggingEntered:(id)info { NSPoint point = [info draggingLocation]; BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; // Don't allow drops that would result in cycles. if (button) { NSData* data = [[info draggingPasteboard] dataForType:kBookmarkButtonDragType]; if (data && [info draggingSource]) { BookmarkButton* sourceButton = nil; [data getBytes:&sourceButton length:sizeof(sourceButton)]; const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; const BookmarkNode* destNode = [button bookmarkNode]; if (destNode->HasAncestor(sourceNode)) button = nil; } } if ([button isFolder]) { if (hoverButton_ == button) { return NSDragOperationMove; // already open or timed to open } if (hoverButton_) { // Oops, another one triggered or open. [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; // Unlike BookmarkBarFolderController, we do not delay the close // of the previous one. Given the lack of diagonal movement, // there is no need, and it feels awkward to do so. See // comments about kDragHoverCloseDelay in // bookmark_bar_folder_controller.mm for more details. [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; hoverButton_.reset(); } hoverButton_.reset([button retain]); DCHECK([[hoverButton_ target] respondsToSelector:@selector(openBookmarkFolderFromButton:)]); [[hoverButton_ target] performSelector:@selector(openBookmarkFolderFromButton:) withObject:hoverButton_ afterDelay:bookmarks::kDragHoverOpenDelay inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } if (!button) { if (hoverButton_) { [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; hoverButton_.reset(); } } // Thrown away but kept to be consistent with the draggingEntered: interface. return NSDragOperationMove; } - (void)draggingExited:(id)info { // Only close the folder menu if the user dragged up past the BMB. If the user // dragged to below the BMB, they might be trying to drop a link into the open // folder menu. // TODO(asvitkine): Need a way to close the menu if the user dragged below but // not into the menu. NSRect bounds = [[self view] bounds]; NSPoint origin = [[self view] convertPoint:bounds.origin toView:nil]; if ([info draggingLocation].y > origin.y + bounds.size.height) [self closeFolderAndStopTrackingMenus]; // NOT the same as a cancel --> we may have moved the mouse into the submenu. if (hoverButton_) { [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; hoverButton_.reset(); } } - (BOOL)dragShouldLockBarVisibility { return ![self isInState:BookmarkBar::DETACHED] && ![self isAnimatingToState:BookmarkBar::DETACHED]; } // TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController. // http://crbug.com/35966 - (BOOL)dragButton:(BookmarkButton*)sourceButton to:(NSPoint)point copy:(BOOL)copy { DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; return [self dragBookmark:sourceNode to:point copy:copy]; } - (BOOL)dragBookmarkData:(id)info { BOOL dragged = NO; std::vector nodes([self retrieveBookmarkNodeData]); if (nodes.size()) { BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); NSPoint dropPoint = [info draggingLocation]; for (std::vector::const_iterator it = nodes.begin(); it != nodes.end(); ++it) { const BookmarkNode* sourceNode = *it; dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; } } return dragged; } - (std::vector)retrieveBookmarkNodeData { std::vector dragDataNodes; BookmarkNodeData dragData; if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) { std::vector nodes( dragData.GetNodes(bookmarkModel_, browser_->profile()->GetPath())); dragDataNodes.assign(nodes.begin(), nodes.end()); } return dragDataNodes; } // Return YES if we should show the drop indicator, else NO. - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { return ![self buttonForDroppingOnAtPoint:point]; } // Return the x position for a drop indicator. - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { CGFloat x = 0; CGFloat halfHorizontalPadding = 0.5 * bookmarks::kBookmarkHorizontalPadding; int destIndex = [self indexForDragToPoint:point]; int numButtons = displayedButtonCount_; CGFloat leftmostX; if (![managedBookmarksButton_ isHidden]) leftmostX = NSMaxX([managedBookmarksButton_ frame]) + halfHorizontalPadding; else if (![appsPageShortcutButton_ isHidden]) leftmostX = NSMaxX([appsPageShortcutButton_ frame]) + halfHorizontalPadding; else leftmostX = bookmarks::kBookmarkLeftMargin - halfHorizontalPadding; // If it's a drop strictly between existing buttons ... if (destIndex == 0) { x = leftmostX; } else if (destIndex > 0 && destIndex < numButtons) { // ... put the indicator right between the buttons. BookmarkButton* button = [buttons_ objectAtIndex:static_cast(destIndex-1)]; DCHECK(button); NSRect buttonFrame = [button frame]; x = NSMaxX(buttonFrame) + halfHorizontalPadding; // If it's a drop at the end (past the last button, if there are any) ... } else if (destIndex == numButtons) { // and if it's past the last button ... if (numButtons > 0) { // ... find the last button, and put the indicator to its right. BookmarkButton* button = [buttons_ objectAtIndex:static_cast(destIndex - 1)]; DCHECK(button); x = NSMaxX([button frame]) + halfHorizontalPadding; // Otherwise, put it right at the beginning. } else { x = leftmostX; } } else { NOTREACHED(); } return x; } - (void)childFolderWillShow:(id)child { // If the bookmarkbar is not in detached mode, lock bar visibility, forcing // the overlay to stay open when in fullscreen mode. if (![self isInState:BookmarkBar::DETACHED] && ![self isAnimatingToState:BookmarkBar::DETACHED]) { BrowserWindowController* browserController = [BrowserWindowController browserWindowControllerForView:[self view]]; [browserController lockBarVisibilityForOwner:child withAnimation:NO delay:NO]; } } - (void)childFolderWillClose:(id)child { // Release bar visibility, allowing the overlay to close if in fullscreen // mode. BrowserWindowController* browserController = [BrowserWindowController browserWindowControllerForView:[self view]]; [browserController releaseBarVisibilityForOwner:child withAnimation:NO delay:NO]; } // Add a new folder controller as triggered by the given folder button. - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { // If doing a close/open, make sure the fullscreen chrome doesn't // have a chance to begin animating away in the middle of things. BrowserWindowController* browserController = [BrowserWindowController browserWindowControllerForView:[self view]]; // Confirm we're not re-locking with ourself as an owner before locking. DCHECK([browserController isBarVisibilityLockedForOwner:self] == NO); [browserController lockBarVisibilityForOwner:self withAnimation:NO delay:NO]; if (folderController_) [self closeAllBookmarkFolders]; // Folder controller, like many window controllers, owns itself. folderController_ = [[BookmarkBarFolderController alloc] initWithParentButton:parentButton parentController:nil barController:self profile:browser_->profile()]; [folderController_ showWindow:self]; // Only BookmarkBarController has this; the // BookmarkBarFolderController does not. [self watchForExitEvent:YES]; // No longer need to hold the lock; the folderController_ now owns it. [browserController releaseBarVisibilityForOwner:self withAnimation:NO delay:NO]; } - (void)openAll:(const BookmarkNode*)node disposition:(WindowOpenDisposition)disposition { [self closeFolderAndStopTrackingMenus]; chrome::OpenAll([[self view] window], browser_, node, disposition, browser_->profile()); } - (void)addButtonForNode:(const BookmarkNode*)node atIndex:(NSInteger)buttonIndex { int newOffset = bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding; if (buttonIndex == -1) buttonIndex = [buttons_ count]; // New button goes at the end. if (buttonIndex <= (NSInteger)[buttons_ count]) { if (buttonIndex) { BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1]; NSRect targetFrame = [targetButton frame]; newOffset = targetFrame.origin.x + NSWidth(targetFrame) + bookmarks::kBookmarkHorizontalPadding; } BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset]; ++displayedButtonCount_; [buttons_ insertObject:newButton atIndex:buttonIndex]; [buttonView_ addSubview:newButton]; [self resetAllButtonPositionsWithAnimation:NO]; // See if any buttons need to be pushed off to or brought in from the side. [self reconfigureBookmarkBar]; } else { // A button from somewhere else (not the bar) is being moved to the // off-the-side so insure it gets redrawn if its showing. [self reconfigureBookmarkBar]; [folderController_ reconfigureMenu]; } } // TODO(mrossetti): Duplicate code with BookmarkBarFolderController. // http://crbug.com/35966 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { DCHECK([urls count] == [titles count]); BOOL nodesWereAdded = NO; // Figure out where these new bookmarks nodes are to be added. BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; const BookmarkNode* destParent = NULL; int destIndex = 0; if ([button isFolder]) { destParent = [button bookmarkNode]; // Drop it at the end. destIndex = [button bookmarkNode]->child_count(); } else { // Else we're dropping somewhere on the bar, so find the right spot. destParent = bookmarkModel_->bookmark_bar_node(); destIndex = [self indexForDragToPoint:point]; } if (!bookmarkClient_->CanBeEditedByUser(destParent)) return NO; // Don't add the bookmarks if the destination index shows an error. if (destIndex >= 0) { // Create and add the new bookmark nodes. size_t urlCount = [urls count]; for (size_t i = 0; i < urlCount; ++i) { GURL gurl; const char* string = [[urls objectAtIndex:i] UTF8String]; if (string) gurl = GURL(string); // We only expect to receive valid URLs. DCHECK(gurl.is_valid()); if (gurl.is_valid()) { bookmarkModel_->AddURL(destParent, destIndex++, base::SysNSStringToUTF16( [titles objectAtIndex:i]), gurl); nodesWereAdded = YES; } } } return nodesWereAdded; } - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { if (fromIndex != toIndex) { NSInteger buttonCount = (NSInteger)[buttons_ count]; if (toIndex == -1) toIndex = buttonCount; // See if we have a simple move within the bar, which will be the case if // both button indexes are in the visible space. if (fromIndex < buttonCount && toIndex < buttonCount) { BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; [buttons_ removeObjectAtIndex:fromIndex]; [buttons_ insertObject:movedButton atIndex:toIndex]; [movedButton setHidden:NO]; [self resetAllButtonPositionsWithAnimation:NO]; } else if (fromIndex < buttonCount) { // A button is being removed from the bar and added to off-the-side. // By now the node has already been inserted into the model so the // button to be added is represented by |toIndex|. Things get // complicated because the off-the-side is showing and must be redrawn // while possibly re-laying out the bookmark bar. [self removeButton:fromIndex animate:NO]; [self reconfigureBookmarkBar]; [folderController_ reconfigureMenu]; } else if (toIndex < buttonCount) { // A button is being added to the bar and removed from off-the-side. // By now the node has already been inserted into the model so the // button to be added is represented by |toIndex|. const BookmarkNode* node = bookmarkModel_->bookmark_bar_node(); const BookmarkNode* movedNode = node->GetChild(toIndex); DCHECK(movedNode); [self addButtonForNode:movedNode atIndex:toIndex]; [self reconfigureBookmarkBar]; } else { // A button is being moved within the off-the-side. fromIndex -= buttonCount; toIndex -= buttonCount; [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex]; } } } - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { if (buttonIndex < (NSInteger)[buttons_ count]) { // The button being removed is showing in the bar. BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; if (oldButton == [folderController_ parentButton]) { // If we are deleting a button whose folder is currently open, close it! [self closeAllBookmarkFolders]; } if (animate && innerContentAnimationsEnabled_ && [self isVisible] && [[self browserWindow] isMainWindow]) { NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation]; NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, NSZeroSize, nil, nil, nil); } [oldButton setDelegate:nil]; [oldButton removeFromSuperview]; [buttons_ removeObjectAtIndex:buttonIndex]; --displayedButtonCount_; [self resetAllButtonPositionsWithAnimation:YES]; [self reconfigureBookmarkBar]; } else if (folderController_ && [folderController_ parentButton] == offTheSideButton_) { // The button being removed is in the OTS (off-the-side) and the OTS // menu is showing so we need to remove the button. NSInteger index = buttonIndex - displayedButtonCount_; [folderController_ removeButton:index animate:animate]; } } - (id)controllerForNode: (const BookmarkNode*)node { // See if it's in the bar, then if it is in the hierarchy of visible // folder menus. if (bookmarkModel_->bookmark_bar_node() == node) return self; return [folderController_ controllerForNode:node]; } @end