summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa/bookmark_bar_controller.mm
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/cocoa/bookmark_bar_controller.mm')
-rw-r--r--chrome/browser/cocoa/bookmark_bar_controller.mm2403
1 files changed, 2403 insertions, 0 deletions
diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm
new file mode 100644
index 0000000..f05f9cd
--- /dev/null
+++ b/chrome/browser/cocoa/bookmark_bar_controller.mm
@@ -0,0 +1,2403 @@
+// Copyright (c) 2010 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/cocoa/bookmark_bar_controller.h"
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_editor.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/browser_list.h"
+#import "chrome/browser/browser_theme_provider.h"
+#import "chrome/browser/cocoa/background_gradient_view.h"
+#import "chrome/browser/cocoa/bookmark_bar_bridge.h"
+#import "chrome/browser/cocoa/bookmark_bar_constants.h"
+#import "chrome/browser/cocoa/bookmark_bar_folder_controller.h"
+#import "chrome/browser/cocoa/bookmark_bar_folder_window.h"
+#import "chrome/browser/cocoa/bookmark_bar_toolbar_view.h"
+#import "chrome/browser/cocoa/bookmark_bar_view.h"
+#import "chrome/browser/cocoa/bookmark_button.h"
+#import "chrome/browser/cocoa/bookmark_button_cell.h"
+#import "chrome/browser/cocoa/bookmark_editor_controller.h"
+#import "chrome/browser/cocoa/bookmark_folder_target.h"
+#import "chrome/browser/cocoa/bookmark_menu.h"
+#import "chrome/browser/cocoa/bookmark_menu_cocoa_controller.h"
+#import "chrome/browser/cocoa/bookmark_name_folder_controller.h"
+#import "chrome/browser/cocoa/browser_window_controller.h"
+#import "chrome/browser/cocoa/event_utils.h"
+#import "chrome/browser/cocoa/fullscreen_controller.h"
+#import "chrome/browser/cocoa/import_settings_dialog.h"
+#import "chrome/browser/cocoa/menu_button.h"
+#import "chrome/browser/cocoa/themed_window.h"
+#import "chrome/browser/cocoa/toolbar_controller.h"
+#import "chrome/browser/cocoa/view_id_util.h"
+#import "chrome/browser/cocoa/view_resizer.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#include "chrome/browser/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view.h"
+#include "chrome/common/pref_names.h"
+#include "grit/app_resources.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+// 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
+// |-updateAndShowNormalBar:showDetachedBar:withAnimation:|. 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:
+// - |-moveToVisualState: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 {
+
+// Overlap (in pixels) between the toolbar and the bookmark bar (when showing in
+// normal mode).
+const CGFloat kBookmarkBarOverlap = 3.0;
+
+// Duration of the bookmark bar animations.
+const NSTimeInterval kBookmarkBarAnimationDuration = 0.12;
+
+} // namespace
+
+@interface BookmarkBarController(Private)
+
+// Determines the appropriate state for the given situation.
++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar;
+
+// 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)moveToVisualState:(bookmarks::VisualState)nextVisualState
+ 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 visual state without animating, stopping a running
+// animation if necessary.
+- (void)finalizeVisualState;
+
+// 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<NSDraggingInfo>. |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)addNonBookmarkButtonsToView;
+- (void)addButtonsToView;
+- (void)centerNoItemsLabel;
+- (void)setNodeForBarMenu;
+
+- (void)watchForExitEvent:(BOOL)watch;
+
+@end
+
+@implementation BookmarkBarController
+
+@synthesize visualState = visualState_;
+@synthesize lastVisualState = lastVisualState_;
+@synthesize delegate = delegate_;
+
+- (id)initWithBrowser:(Browser*)browser
+ initialWidth:(float)initialWidth
+ delegate:(id<BookmarkBarControllerDelegate>)delegate
+ resizeDelegate:(id<ViewResizer>)resizeDelegate {
+ if ((self = [super initWithNibName:@"BookmarkBar"
+ bundle:mac_util::MainAppBundle()])) {
+ // Initialize to an invalid state.
+ visualState_ = bookmarks::kInvalidState;
+ lastVisualState_ = bookmarks::kInvalidState;
+
+ browser_ = browser;
+ initialWidth_ = initialWidth;
+ bookmarkModel_ = browser_->profile()->GetBookmarkModel();
+ buttons_.reset([[NSMutableArray alloc] init]);
+ delegate_ = delegate;
+ resizeDelegate_ = resizeDelegate;
+ folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ folderImage_.reset([rb.GetNSImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]);
+ defaultImage_.reset([rb.GetNSImageNamed(IDR_DEFAULT_FAVICON) retain]);
+
+ // Register for theme changes.
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ // This call triggers an awakeFromNib, which builds the bar, which
+ // might uses folderImage_. So make sure it happens after
+ // folderImage_ is loaded.
+ [[self animatableView] setResizeDelegate:resizeDelegate];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // 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.
+ [offTheSideButton_ setDraggable:NO];
+
+ // 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_];
+
+ // Copy the bar menu so we know if it's from the bar or a folder.
+ // Then we set its represented item to be the bookmark bar.
+ buttonFolderContextMenu_.reset([[[self view] menu] copy]);
+
+ // 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(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:NSWindowDidResignKeyNotification
+ object:nil];
+
+ [defaultCenter addObserver:self
+ selector:@selector(parentWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:[[self view] window]];
+ [defaultCenter addObserver:self
+ selector:@selector(parentWindowDidResignKey:)
+ name:NSWindowDidResignKeyNotification
+ 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)parentWindowDidResignKey:(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));
+
+ // The state of our morph (if any); 1 is total bubble, 0 is the regular bar.
+ CGFloat morph = [self detachedMorphProgress];
+
+ // Add padding to the detached bookmark bar.
+ buttonViewFrame = NSInsetRect(buttonViewFrame,
+ morph * bookmarks::kNTPBookmarkBarPadding,
+ morph * bookmarks::kNTPBookmarkBarPadding);
+
+ [buttonView_ setFrame:buttonViewFrame];
+}
+
+// We don't change a preference; we only change visibility. Preference changing
+// (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We
+// simply update based on what we're told.
+- (void)updateVisibility {
+ [self showBookmarkBarWithAnimation:NO];
+}
+
+- (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:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState] ||
+ [self isAnimatingBetweenState:bookmarks::kShowingState
+ andState:bookmarks::kDetachedState])
+ return 0;
+
+ // If we ever need any other animation cases, code would go here.
+ }
+
+ return [self isInState:bookmarks::kShowingState] ? 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:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState])
+ 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:bookmarks::kShowingState
+ andState:bookmarks::kDetachedState])
+ return static_cast<CGFloat>([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:bookmarks::kShowingState] ? 0 : 1;
+}
+
+- (NSImage*)favIconForNode:(const BookmarkNode*)node {
+ if (!node)
+ return defaultImage_;
+
+ if (node->is_folder())
+ return folderImage_;
+
+ const SkBitmap& favIcon = bookmarkModel_->GetFavIcon(node);
+ if (!favIcon.isNull())
+ return gfx::SkBitmapToNSImage(favIcon);
+
+ return defaultImage_;
+}
+
+- (void)closeFolderAndStopTrackingMenus {
+ showFolderMenus_ = NO;
+ [self closeAllBookmarkFolders];
+}
+
+#pragma mark Actions
+
+- (IBAction)openBookmark:(id)sender {
+ [self closeFolderAndStopTrackingMenus];
+ DCHECK([sender respondsToSelector:@selector(bookmarkNode)]);
+ const BookmarkNode* node = [sender bookmarkNode];
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ [self openURL:node->GetURL() disposition:disposition];
+}
+
+// Redirect to our logic shared with BookmarkBarFolderController.
+- (IBAction)openBookmarkFolderFromButton:(id)sender {
+ DCHECK(sender != offTheSideButton_);
+ // Toggle presentation of bar folder menus.
+ showFolderMenus_ = !showFolderMenus_;
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+// The button that sends this one is special; the "off the side"
+// button (chevron) opens like a folder button but isn't exactly a
+// parent folder.
+- (IBAction)openOffTheSideFolderFromButton:(id)sender {
+ DCHECK([sender isKindOfClass:[BookmarkButton class]]);
+ DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]);
+ [[sender cell] setStartingChildIndex:displayedButtonCount_];
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+- (IBAction)openBookmarkInNewForegroundTab:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node)
+ [self openURL:node->GetURL() disposition:NEW_FOREGROUND_TAB];
+ [self closeAllBookmarkFolders];
+}
+
+- (IBAction)openBookmarkInNewWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node)
+ [self openURL:node->GetURL() disposition:NEW_WINDOW];
+}
+
+- (IBAction)openBookmarkInIncognitoWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node)
+ [self openURL:node->GetURL() disposition:OFF_THE_RECORD];
+}
+
+- (IBAction)editBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (!node)
+ return;
+
+ if (node->is_folder()) {
+ BookmarkNameFolderController* controller =
+ [[BookmarkNameFolderController alloc]
+ initWithParentWindow:[[self view] window]
+ profile:browser_->profile()
+ node:node];
+ [controller runAsModalSheet];
+ return;
+ }
+
+ // There is no real need to jump to a platform-common routine at
+ // this point (which just jumps back to objc) other than consistency
+ // across platforms.
+ //
+ // TODO(jrg): identify when we NO_TREE. I can see it in the code
+ // for the other platforms but can't find a way to trigger it in the
+ // UI.
+ BookmarkEditor::Show([[self view] window],
+ browser_->profile(),
+ node->GetParent(),
+ BookmarkEditor::EditDetails(node),
+ BookmarkEditor::SHOW_TREE);
+}
+
+- (IBAction)cutBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ std::vector<const BookmarkNode*> nodes;
+ nodes.push_back(node);
+ bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, true);
+ }
+}
+
+- (IBAction)copyBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ std::vector<const BookmarkNode*> nodes;
+ nodes.push_back(node);
+ bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, false);
+ }
+}
+
+// Paste the copied node immediately after the node for which the context
+// menu has been presented if the node is a non-folder bookmark, otherwise
+// past at the end of the folder node.
+- (IBAction)pasteBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ int index = -1;
+ if (node != bookmarkModel_->GetBookmarkBarNode() && !node->is_folder()) {
+ const BookmarkNode* parent = node->GetParent();
+ index = parent->IndexOfChild(node) + 1;
+ if (index > parent->GetChildCount())
+ index = -1;
+ node = parent;
+ }
+ bookmark_utils::PasteFromClipboard(bookmarkModel_, node, index);
+ }
+}
+
+- (IBAction)deleteBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ bookmarkModel_->Remove(node->GetParent(),
+ node->GetParent()->IndexOfChild(node));
+ }
+}
+
+- (IBAction)openAllBookmarks:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ [self openAll:node disposition:NEW_FOREGROUND_TAB];
+ UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarks"),
+ browser_->profile());
+ }
+}
+
+- (IBAction)openAllBookmarksNewWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ [self openAll:node disposition:NEW_WINDOW];
+ UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarksNewWindow"),
+ browser_->profile());
+ }
+}
+
+- (IBAction)openAllBookmarksIncognitoWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ [self openAll:node disposition:OFF_THE_RECORD];
+ UserMetrics::RecordAction(
+ UserMetricsAction("OpenAllBookmarksIncognitoWindow"),
+ browser_->profile());
+ }
+}
+
+// May be called from the bar or from a folder button.
+// If called from a button, that button becomes the parent.
+- (IBAction)addPage:(id)sender {
+ const BookmarkNode* parent = [self nodeFromMenuItem:sender];
+ if (!parent)
+ parent = bookmarkModel_->GetBookmarkBarNode();
+ BookmarkEditor::Show([[self view] window],
+ browser_->profile(),
+ parent,
+ BookmarkEditor::EditDetails(),
+ BookmarkEditor::SHOW_TREE);
+}
+
+// Might be called from the context menu over the bar OR over a
+// button. If called from a button, that button becomes a sibling of
+// the new node. If called from the bar, add to the end of the bar.
+- (IBAction)addFolder:(id)sender {
+ const BookmarkNode* senderNode = [self nodeFromMenuItem:sender];
+ const BookmarkNode* parent = NULL;
+ int newIndex = 0;
+ // If triggered from the bar, folder or "others" folder - add as a child to
+ // the end.
+ // If triggered from a bookmark, add as next sibling.
+ BookmarkNode::Type type = senderNode->type();
+ if (type == BookmarkNode::BOOKMARK_BAR ||
+ type == BookmarkNode::OTHER_NODE ||
+ type == BookmarkNode::FOLDER) {
+ parent = senderNode;
+ newIndex = parent->GetChildCount();
+ } else {
+ parent = senderNode->GetParent();
+ newIndex = parent->IndexOfChild(senderNode) + 1;
+ }
+ BookmarkNameFolderController* controller =
+ [[BookmarkNameFolderController alloc]
+ initWithParentWindow:[[self view] window]
+ profile:browser_->profile()
+ parent:parent
+ newIndex:newIndex];
+ [controller runAsModalSheet];
+}
+
+- (IBAction)importBookmarks:(id)sender {
+ [ImportSettingsDialogController showImportSettingsDialogForProfile:
+ browser_->profile()];
+}
+
+#pragma mark Private Methods
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)aNotification {
+ ThemeProvider* themeProvider =
+ static_cast<ThemeProvider*>([[aNotification object] pointerValue]);
+ [self updateTheme: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];
+}
+
+// Position the off-the-side chevron to the left of the otherBookmarks button.
+- (void)positionOffTheSideButton {
+ NSRect frame = [offTheSideButton_ frame];
+ if (otherBookmarksButton_.get()) {
+ frame.origin.x = ([otherBookmarksButton_ frame].origin.x -
+ (frame.size.width +
+ bookmarks::kBookmarkHorizontalPadding));
+ [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 {
+ // If deleting a button while off-the-side is open, buttons may be
+ // promoted from off-the-side to the bar. Accomodate.
+ if (folderController_ &&
+ ([folderController_ parentButton] == offTheSideButton_)) {
+ [folderController_ reconfigureMenu];
+ }
+
+ [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_];
+ [[offTheSideButton_ cell]
+ setBookmarkNode:bookmarkModel_->GetBookmarkBarNode()];
+ int bookmarkChildren = bookmarkModel_->GetBookmarkBarNode()->GetChildCount();
+ 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];
+ }
+}
+
+// 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 {
+ CrApplication* app = static_cast<CrApplication*>([NSApplication
+ sharedApplication]);
+ DCHECK([app isKindOfClass:[CrApplication class]]);
+ if (watch) {
+ if (!watchingForExitEvent_)
+ [app addEventHook:self];
+ } else {
+ if (watchingForExitEvent_)
+ [app removeEventHook:self];
+ }
+ watchingForExitEvent_ = watch;
+}
+
+// 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 && !ignoreAnimations_) {
+ // 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:bookmarks::kShowingState];
+ [[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:bookmarks::kHiddenState
+ toState:bookmarks::kShowingState]) {
+ [[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:(bookmarks::kBookmarkBarHeight -
+ kBookmarkBarOverlap)
+ duration:kBookmarkBarAnimationDuration];
+ } else if ([self isAnimatingFromState:bookmarks::kShowingState
+ toState:bookmarks::kHiddenState]) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ [view animateToNewHeight:0
+ duration:kBookmarkBarAnimationDuration];
+ } else if ([self isAnimatingFromState:bookmarks::kShowingState
+ toState:bookmarks::kDetachedState]) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ [view animateToNewHeight:bookmarks::kNTPBookmarkBarHeight
+ duration:kBookmarkBarAnimationDuration];
+ } else if ([self isAnimatingFromState:bookmarks::kDetachedState
+ toState:bookmarks::kShowingState]) {
+ [[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:(bookmarks::kBookmarkBarHeight -
+ kBookmarkBarOverlap)
+ duration:kBookmarkBarAnimationDuration];
+ } else {
+ // Oops! An animation we don't know how to handle.
+ return NO;
+ }
+
+ return YES;
+}
+
+// Enable or disable items. We are the menu delegate for both the bar
+// and for bookmark folder buttons.
+- (BOOL)validateUserInterfaceItem:(id)item {
+ // Yes for everything we don't explicitly deny.
+ if (![item isKindOfClass:[NSMenuItem class]])
+ return YES;
+
+ // Yes if we're not a special BookmarkMenu.
+ if (![[item menu] isKindOfClass:[BookmarkMenu class]])
+ return YES;
+
+ // No if we think it's a special BookmarkMenu but have trouble.
+ const BookmarkNode* node = [self nodeFromMenuItem:item];
+ if (!node)
+ return NO;
+
+ // If this is the bar menu, we only have things to do if there are
+ // buttons. If this is a folder button menu, we only have things to
+ // do if the folder has items.
+ NSMenu* menu = [item menu];
+ BOOL thingsToDo = NO;
+ if (menu == [[self view] menu]) {
+ thingsToDo = [buttons_ count] ? YES : NO;
+ } else {
+ if (node && node->is_folder() && node->GetChildCount()) {
+ thingsToDo = YES;
+ }
+ }
+
+ // Disable openAll* if we have nothing to do.
+ SEL action = [item action];
+ if ((!thingsToDo) &&
+ ((action == @selector(openAllBookmarks:)) ||
+ (action == @selector(openAllBookmarksNewWindow:)) ||
+ (action == @selector(openAllBookmarksIncognitoWindow:)))) {
+ return NO;
+ }
+
+ if ((action == @selector(editBookmark:)) ||
+ (action == @selector(deleteBookmark:)) ||
+ (action == @selector(cutBookmark:)) ||
+ (action == @selector(copyBookmark:))) {
+ // Don't allow edit/delete of the bar node, or of "Other Bookmarks"
+ if ((node == nil) ||
+ (node == bookmarkModel_->other_node()) ||
+ (node == bookmarkModel_->GetBookmarkBarNode())) {
+ return NO;
+ }
+ }
+
+ if (action == @selector(pasteBookmark:) &&
+ !bookmark_utils::CanPasteFromClipboard(node))
+ return NO;
+
+ // If this is an incognito window, don't allow "open in incognito".
+ if ((action == @selector(openBookmarkInIncognitoWindow:)) ||
+ (action == @selector(openAllBookmarksIncognitoWindow:))) {
+ if (browser_->profile()->IsOffTheRecord()) {
+ return NO;
+ }
+ }
+
+ // Enabled by default.
+ return YES;
+}
+
+// Actually open the URL. This is the last chance for a unit test to
+// override.
+- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition {
+ browser_->OpenURL(url, GURL(), disposition, PageTransition::AUTO_BOOKMARK);
+}
+
+- (void)clearMenuTagMap {
+ seedId_ = 0;
+ menuTagMap_.clear();
+}
+
+- (int)preferredHeight {
+ DCHECK(![self isAnimationRunning]);
+
+ if (!barIsEnabled_)
+ return 0;
+
+ switch (visualState_) {
+ case bookmarks::kShowingState:
+ return bookmarks::kBookmarkBarHeight;
+ case bookmarks::kDetachedState:
+ return bookmarks::kNTPBookmarkBarHeight;
+ case bookmarks::kHiddenState:
+ return 0;
+ case bookmarks::kInvalidState:
+ default:
+ NOTREACHED();
+ 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->GetChildCount()) {
+ [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()]];
+ // Add a tooltip
+ std::string url_string = child->GetURL().possibly_invalid_spec();
+ NSString* tooltip = [NSString stringWithFormat:@"%@\n%s",
+ base::SysWideToNSString(child->GetTitle()),
+ url_string.c_str()];
+ [item setToolTip:tooltip];
+ }
+}
+
+// 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->GetChildCount(); 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::SysWideToNSString(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 = bookmarkModel_->GetNodeByID(tag);
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ [self openURL:node->GetURL() 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->GetChildCount() > 0;
+ [[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 = 0;
+ for (int i = 0; i < node->GetChildCount(); i++) {
+ const BookmarkNode* child = node->GetChild(i);
+ BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
+ if (NSMinX([button frame]) >= maxViewX)
+ break;
+ [buttons_ addObject:button];
+ }
+}
+
+- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
+ xOffset:(int*)xOffset {
+ BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
+ NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset];
+
+ scoped_nsobject<BookmarkButton>
+ 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.)
+ ThemeProvider* themeProvider = [[[self view] window] themeProvider];
+ if (themeProvider) {
+ NSColor* color =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT,
+ true);
+ [cell setTextColor:color];
+ }
+
+ if (node->is_folder()) {
+ [button setTarget:self];
+ [button setAction:@selector(openBookmarkFolderFromButton:)];
+ } else {
+ // Make the button do something
+ [button setTarget:self];
+ [button setAction:@selector(openBookmark:)];
+ // Add a tooltip.
+ NSString* title = base::SysWideToNSString(node->GetTitle());
+ std::string url_string = node->GetURL().possibly_invalid_spec();
+ NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title,
+ url_string.c_str()];
+ [button setToolTip:tooltip];
+ }
+ return [[button.get() retain] autorelease];
+}
+
+// Add non-bookmark buttons to the view. This includes the chevron
+// and the "other bookmarks" button. Technically "other bookmarks" is
+// a bookmark button but it is treated specially. Only needs to be
+// called when these buttons are new or when the bookmark bar is
+// cleared (e.g. on a loaded: call). Unlike addButtonsToView below,
+// we don't need to add/remove these dynamically in response to window
+// resize.
+- (void)addNonBookmarkButtonsToView {
+ [buttonView_ addSubview:otherBookmarksButton_.get()];
+ [buttonView_ addSubview:offTheSideButton_];
+}
+
+// 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];
+ }
+}
+
+// Create the button for "Other Bookmarks" on the right of the bar.
+- (void)createOtherBookmarksButton {
+ // Can't create this until the model is loaded, but only need to
+ // create it once.
+ if (otherBookmarksButton_.get())
+ return;
+
+ // TODO(jrg): remove duplicate code
+ NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()];
+ int ignored = 0;
+ NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:&ignored];
+ frame.origin.x = [[self buttonView] bounds].size.width - frame.size.width;
+ frame.origin.x -= bookmarks::kBookmarkHorizontalPadding;
+ BookmarkButton* button = [[BookmarkButton alloc] initWithFrame:frame];
+ [button setDraggable:NO];
+ otherBookmarksButton_.reset(button);
+ view_id_util::SetID(button, VIEW_ID_OTHER_BOOKMARKS);
+
+ // Make sure this button, like all other BookmarkButtons, lives
+ // until the end of the current event loop.
+ [[button retain] autorelease];
+
+ // Peg at right; keep same height as bar.
+ [button setAutoresizingMask:(NSViewMinXMargin)];
+ [button setCell:cell];
+ [button setDelegate:self];
+ [button setTarget:self];
+ [button setAction:@selector(openBookmarkFolderFromButton:)];
+ [buttonView_ addSubview:button];
+
+ // Now that it's here, move the chevron over.
+ [self positionOffTheSideButton];
+}
+
+// Now that the model is loaded, set the bookmark bar root as the node
+// represented by the bookmark bar (default, background) menu.
+- (void)setNodeForBarMenu {
+ const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode();
+ BookmarkMenu* menu = static_cast<BookmarkMenu*>([[self view] menu]);
+
+ // Make sure types are compatible
+ DCHECK(sizeof(long long) == sizeof(int64));
+ [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]];
+}
+
+// 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 {
+ [buttonContextMenu_ cancelTracking];
+ [buttonFolderContextMenu_ cancelTracking];
+}
+
+// Determines the appropriate state for the given situation.
++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar {
+ if (showNormalBar)
+ return bookmarks::kShowingState;
+ if (showDetachedBar)
+ return bookmarks::kDetachedState;
+ return bookmarks::kHiddenState;
+}
+
+- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState
+ 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 ((nextVisualState == visualState_) && (!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 (nextVisualState != lastVisualState_) {
+ [self stopCurrentAnimation];
+ [self finalizeVisualState];
+ }
+
+ // If we're in case [*] above, we can stop here.
+ if (nextVisualState == visualState_)
+ return;
+ }
+
+ // Now update with the new state change.
+ lastVisualState_ = visualState_;
+ visualState_ = nextVisualState;
+
+ // Animate only if told to and if bar is enabled.
+ if (animate && !ignoreAnimations_ && 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:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState] ||
+ [self isAnimatingBetweenState:bookmarks::kShowingState
+ andState:bookmarks::kDetachedState]) {
+ [delegate_ bookmarkBar:self willAnimateFromState:lastVisualState_
+ toState:visualState_];
+ [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 finalizeVisualState];
+}
+
+// N.B.: |-moveToVisualState:...| will check if this should be a no-op or not.
+- (void)updateAndShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar
+ withAnimation:(BOOL)animate {
+ bookmarks::VisualState newVisualState =
+ [BookmarkBarController visualStateToShowNormalBar:showNormalBar
+ showDetachedBar:showDetachedBar];
+ [self moveToVisualState:newVisualState
+ withAnimation:animate && !ignoreAnimations_];
+}
+
+// (Private)
+- (void)finalizeVisualState {
+ // We promise that our delegate that the variables will be finalized before
+ // the call to |-bookmarkBar:didChangeFromState:toState:|.
+ bookmarks::VisualState oldVisualState = lastVisualState_;
+ lastVisualState_ = bookmarks::kInvalidState;
+
+ // Notify our delegate.
+ [delegate_ bookmarkBar:self didChangeFromState:oldVisualState
+ toState:visualState_];
+
+ // Update ourselves visually.
+ [self updateVisibility];
+}
+
+// (Private)
+- (void)stopCurrentAnimation {
+ [[self animatableView] stopAnimation];
+}
+
+// Delegate method for |AnimatableView| (a superclass of
+// |BookmarkBarToolbarView|).
+- (void)animationDidEnd:(NSAnimation*)animation {
+ [self finalizeVisualState];
+}
+
+- (void)reconfigureBookmarkBar {
+ [self redistributeButtonsOnBarAsNeeded];
+ [self positionOffTheSideButton];
+ [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;
+}
+
+// Adjust the horizontal width 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)adjustNoItemContainerWidthsForMaxX:(CGFloat)maxViewX {
+ if (![[buttonView_ noItemContainer] isHidden]) {
+ // Reset initial frames for the two items, then adjust as necessary.
+ NSTextField* noItemTextfield = [buttonView_ noItemTextfield];
+ [noItemTextfield setFrame:originalNoItemsRect_];
+ [noItemTextfield setHidden:NO];
+ NSButton* importBookmarksButton = [buttonView_ importBookmarksButton];
+ [importBookmarksButton setFrame:originalImportBookmarksRect_];
+ [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];
+ }
+}
+
+- (void)redistributeButtonsOnBarAsNeeded {
+ const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode();
+ NSInteger barCount = node->GetChildCount();
+
+ // Determine the current maximum extent of the visible buttons.
+ CGFloat maxViewX = NSMaxX([[self view] bounds]);
+ NSButton* otherBookmarksButton = otherBookmarksButton_.get();
+ // If necessary, pull in the width to account for the Other Bookmarks button.
+ if (otherBookmarksButton_)
+ maxViewX = [otherBookmarksButton frame].origin.x -
+ bookmarks::kBookmarkHorizontalPadding;
+ // If we're already overflowing, then we need to account for the chevron.
+ if (barCount > displayedButtonCount_)
+ maxViewX = [offTheSideButton_ frame].origin.x -
+ bookmarks::kBookmarkHorizontalPadding;
+
+ // 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([button frame]) < maxViewX)
+ break;
+ [buttons_ removeLastObject];
+ [button removeFromSuperview];
+ --displayedButtonCount_;
+ }
+
+ // As a result of cutting, deleting and dragging, the bar may now have room
+ // for more buttons.
+ int xOffset = displayedButtonCount_ > 0 ?
+ NSMaxX([[buttons_ lastObject] frame]) +
+ bookmarks::kBookmarkHorizontalPadding : 0;
+ 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 += NSWidth([offTheSideButton_ frame]) +
+ bookmarks::kBookmarkHorizontalPadding;
+ if (NSMaxX([button frame]) >= maxViewX)
+ 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 adjustNoItemContainerWidthsForMaxX:maxViewX];
+}
+
+#pragma mark Private Methods Exposed for Testing
+
+- (BookmarkBarView*)buttonView {
+ return buttonView_;
+}
+
+- (NSMutableArray*)buttons {
+ return buttons_.get();
+}
+
+- (NSButton*)offTheSideButton {
+ return offTheSideButton_;
+}
+
+- (BOOL)offTheSideButtonIsHidden {
+ return [offTheSideButton_ isHidden];
+}
+
+- (NSButton*)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 {
+ [buttons_ makeObjectsPerformSelector:@selector(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;
+ NSMenu* menu = node && node->is_folder() ? buttonFolderContextMenu_ :
+ buttonContextMenu_;
+ BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node
+ contextMenu:menu
+ cellText:nil
+ cellImage:image];
+ [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*)button {
+ NSRect frame = [button frame];
+ CGFloat desiredSize = [self widthForBookmarkButtonCell:[button cell]];
+ CGFloat delta = desiredSize - frame.size.width;
+ if (delta) {
+ frame.size.width = desiredSize;
+ [button 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_->IsLoaded())
+ 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;
+}
+
+// Return the BookmarkNode associated with the given NSMenuItem. Can
+// return NULL which means "do nothing". One case where it would
+// return NULL is if the bookmark model gets modified while you have a
+// context menu open.
+- (const BookmarkNode*)nodeFromMenuItem:(id)sender {
+ const BookmarkNode* node = NULL;
+ BookmarkMenu* menu = (BookmarkMenu*)[sender menu];
+ if ([menu isKindOfClass:[BookmarkMenu class]]) {
+ int64 id = [menu id];
+ node = bookmarkModel_->GetNodeByID(id);
+ }
+ return node;
+}
+
+// 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:(ThemeProvider*)themeProvider {
+ if (!themeProvider)
+ return;
+ NSColor* color =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT,
+ true);
+ for (BookmarkButton* button in buttons_.get()) {
+ BookmarkButtonCell* cell = [button cell];
+ [cell setTextColor:color];
+ }
+ [[otherBookmarksButton_ 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:
+ case NSKeyUp:
+ // Any key press ends things.
+ return YES;
+ default:
+ break;
+ }
+ return NO;
+}
+
+#pragma mark Drag & Drop
+
+// Find something like std::is_between<T>? 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) {
+ // 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]];
+ 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->GetParent()->IndexOfChild(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]->GetChildCount();
+ } else {
+ // Else we're dropping somewhere on the bar, so find the right spot.
+ destParent = bookmarkModel_->GetBookmarkBarNode();
+ destIndex = [self indexForDragToPoint:point];
+ }
+
+ // 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<NSDraggingInfo>)info {
+ [self closeFolderAndStopTrackingMenus];
+}
+
+#pragma mark Bridge Notification Handlers
+
+// TODO(jrg): for now this is brute force.
+- (void)loaded:(BookmarkModel*)model {
+ DCHECK(model == bookmarkModel_);
+ if (!model->IsLoaded())
+ 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->GetBookmarkBarNode();
+ [self clearBookmarkBar];
+ [self addNodesToButtonList:node];
+ [self createOtherBookmarksButton];
+ [self updateTheme:[[[self view] window] themeProvider]];
+ [self positionOffTheSideButton];
+ [self addNonBookmarkButtonsToView];
+ [self addButtonsToView];
+ [self configureOffTheSideButtonContentsAndVisibility];
+ [self setNodeForBarMenu];
+}
+
+- (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<BookmarkButtonControllerProtocol> 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->GetBookmarkBarNode()];
+}
+
+// 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<BookmarkButtonControllerProtocol> oldController =
+ [self controllerForNode:oldParent];
+ id<BookmarkButtonControllerProtocol> 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->GetBookmarkBarNode()];
+ // If we moved the only item on the "off the side" menu somewhere
+ // else, we may no longer need to show it.
+ [self configureOffTheSideButtonContentsAndVisibility];
+}
+
+- (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<BookmarkButtonControllerProtocol> 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->GetBookmarkBarNode()];
+ // If we deleted the only item on the "off the side" menu we no
+ // longer need to show it.
+ [self configureOffTheSideButtonContentsAndVisibility];
+}
+
+// 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:nil
+ 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];
+ }
+ }
+}
+
+// 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_ && (visualState_ == bookmarks::kShowingState ||
+ visualState_ == bookmarks::kDetachedState ||
+ lastVisualState_ == bookmarks::kShowingState ||
+ lastVisualState_ == bookmarks::kDetachedState);
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimationRunning {
+ return lastVisualState_ != bookmarks::kInvalidState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isInState:(bookmarks::VisualState)state {
+ return visualState_ == state &&
+ lastVisualState_ == bookmarks::kInvalidState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingToState:(bookmarks::VisualState)state {
+ return visualState_ == state &&
+ lastVisualState_ != bookmarks::kInvalidState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state {
+ return lastVisualState_ == state;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState
+ toState:(bookmarks::VisualState)toState {
+ return lastVisualState_ == fromState && visualState_ == toState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState
+ andState:(bookmarks::VisualState)toState {
+ return (lastVisualState_ == fromState && visualState_ == toState) ||
+ (visualState_ == fromState && lastVisualState_ == toState);
+}
+
+// (BookmarkBarState protocol)
+- (CGFloat)detachedMorphProgress {
+ if ([self isInState:bookmarks::kDetachedState]) {
+ return 1;
+ }
+ if ([self isAnimatingToState:bookmarks::kDetachedState]) {
+ return static_cast<CGFloat>(
+ [[self animatableView] currentAnimationProgress]);
+ }
+ if ([self isAnimatingFromState:bookmarks::kDetachedState]) {
+ return static_cast<CGFloat>(
+ 1 - [[self animatableView] currentAnimationProgress]);
+ }
+ return 0;
+}
+
+#pragma mark BookmarkBarToolbarViewController Protocol
+
+- (int)currentTabContentsHeight {
+ return browser_->GetSelectedTabContents() ?
+ browser_->GetSelectedTabContents()->view()->GetContainerSize().height() :
+ 0;
+}
+
+- (ThemeProvider*)themeProvider {
+ return browser_->profile()->GetThemeProvider();
+}
+
+#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 automically 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.
+ if ([sender bookmarkNode]->is_folder()) {
+ [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];
+}
+
+#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_;
+}
+
+// TODO(jrg): much of this logic is duped with
+// [BookmarkBarFolderController draggingEntered:] except when noted.
+// http://crbug.com/35966
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)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];
+ }
+ 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<NSDraggingInfo>)info {
+ // 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:bookmarks::kDetachedState] &&
+ ![self isAnimatingToState:bookmarks::kDetachedState];
+}
+
+// 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<NSDraggingInfo>)info {
+ BOOL dragged = NO;
+ std::vector<const BookmarkNode*> nodes([self retrieveBookmarkDragDataNodes]);
+ if (nodes.size()) {
+ BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
+ NSPoint dropPoint = [info draggingLocation];
+ for (std::vector<const BookmarkNode*>::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<const BookmarkNode*>)retrieveBookmarkDragDataNodes {
+ std::vector<const BookmarkNode*> dragDataNodes;
+ BookmarkDragData dragData;
+ if(dragData.ReadFromDragClipboard()) {
+ BookmarkModel* bookmarkModel = [self bookmarkModel];
+ Profile* profile = bookmarkModel->profile();
+ std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile));
+ 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;
+ int destIndex = [self indexForDragToPoint:point];
+ int numButtons = displayedButtonCount_;
+
+ // If it's a drop strictly between existing buttons ...
+ if (destIndex >= 0 && destIndex < numButtons) {
+ // ... put the indicator right between the buttons.
+ BookmarkButton* button =
+ [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)];
+ DCHECK(button);
+ NSRect buttonFrame = [button frame];
+ x = buttonFrame.origin.x - 0.5 * bookmarks::kBookmarkHorizontalPadding;
+
+ // 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<NSUInteger>(destIndex - 1)];
+ DCHECK(button);
+ NSRect buttonFrame = [button frame];
+ x = NSMaxX(buttonFrame) + 0.5 * bookmarks::kBookmarkHorizontalPadding;
+
+ // Otherwise, put it right at the beginning.
+ } else {
+ x = 0.5 * bookmarks::kBookmarkHorizontalPadding;
+ }
+ } else {
+ NOTREACHED();
+ }
+
+ return x;
+}
+
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)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:bookmarks::kDetachedState] &&
+ ![self isAnimatingToState:bookmarks::kDetachedState]) {
+ BrowserWindowController* browserController =
+ [BrowserWindowController browserWindowControllerForView:[self view]];
+ [browserController lockBarVisibilityForOwner:child
+ withAnimation:NO
+ delay:NO];
+ }
+}
+
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)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 (folderController_)
+ [self closeAllBookmarkFolders];
+
+ // Folder controller, like many window controllers, owns itself.
+ folderController_ =
+ [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
+ parentController:nil
+ barController:self];
+ [folderController_ showWindow:self];
+
+ // Only BookmarkBarController has this; the
+ // BookmarkBarFolderController does not.
+ [self watchForExitEvent:YES];
+}
+
+- (void)openAll:(const BookmarkNode*)node
+ disposition:(WindowOpenDisposition)disposition {
+ [self closeFolderAndStopTrackingMenus];
+ bookmark_utils::OpenAll([[self view] window],
+ browser_->profile(),
+ browser_,
+ node,
+ disposition);
+}
+
+- (void)addButtonForNode:(const BookmarkNode*)node
+ atIndex:(NSInteger)buttonIndex {
+ int newOffset = 0;
+ 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];
+ CGFloat xOffset =
+ NSWidth([newButton frame]) + bookmarks::kBookmarkHorizontalPadding;
+ NSUInteger buttonCount = [buttons_ count];
+ for (NSUInteger i = buttonIndex; i < buttonCount; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSPoint buttonOrigin = [button frame].origin;
+ buttonOrigin.x += xOffset;
+ [button setFrameOrigin:buttonOrigin];
+ }
+ ++displayedButtonCount_;
+ [buttons_ insertObject:newButton atIndex:buttonIndex];
+ [buttonView_ addSubview:newButton];
+
+ // 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]->GetChildCount();
+ } else {
+ // Else we're dropping somewhere on the bar, so find the right spot.
+ destParent = bookmarkModel_->GetBookmarkBarNode();
+ destIndex = [self indexForDragToPoint:point];
+ }
+
+ // 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::SysNSStringToWide([titles
+ objectAtIndex:i]),
+ gurl);
+ nodesWereAdded = YES;
+ }
+ }
+ }
+ return nodesWereAdded;
+}
+
+// TODO(mrossetti): jrg wants this broken up into smaller functions.
+- (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];
+ NSRect movedFrame = [movedButton frame];
+ NSPoint toOrigin = movedFrame.origin;
+ CGFloat xOffset =
+ NSWidth(movedFrame) + bookmarks::kBookmarkHorizontalPadding;
+ // Hide the button to reduce flickering while drawing the window.
+ [movedButton setHidden:YES];
+ [buttons_ removeObjectAtIndex:fromIndex];
+ if (fromIndex < toIndex) {
+ // Move the button from left to right within the bar.
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
+ NSRect toFrame = [targetButton frame];
+ toOrigin.x = toFrame.origin.x - NSWidth(movedFrame) + NSWidth(toFrame);
+ for (NSInteger i = fromIndex; i < toIndex; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect frame = [button frame];
+ frame.origin.x -= xOffset;
+ [button setFrameOrigin:frame.origin];
+ }
+ } else {
+ // Move the button from right to left within the bar.
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
+ toOrigin = [targetButton frame].origin;
+ for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x += xOffset;
+ [button setFrameOrigin:buttonFrame.origin];
+ }
+ }
+ [buttons_ insertObject:movedButton atIndex:toIndex];
+ [movedButton setFrameOrigin:toOrigin];
+ [movedButton setHidden: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_->GetBookmarkBarNode();
+ 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];
+ }
+ NSRect poofFrame = [oldButton bounds];
+ NSPoint poofPoint = NSMakePoint(NSMidX(poofFrame), NSMidY(poofFrame));
+ poofPoint = [oldButton convertPoint:poofPoint toView:nil];
+ poofPoint = [[oldButton window] convertBaseToScreen:poofPoint];
+ NSRect oldFrame = [oldButton frame];
+ [oldButton removeFromSuperview];
+ if (animate && !ignoreAnimations_ && [self isVisible])
+ NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
+ NSZeroSize, nil, nil, nil);
+ CGFloat xOffset = NSWidth(oldFrame) + bookmarks::kBookmarkHorizontalPadding;
+ [buttons_ removeObjectAtIndex:buttonIndex];
+ NSUInteger buttonCount = [buttons_ count];
+ for (NSUInteger i = buttonIndex; i < buttonCount; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x -= xOffset;
+ [button setFrame:buttonFrame];
+ // If this button is showing its menu then we need to move the menu, too.
+ if (button == [folderController_ parentButton])
+ [folderController_ offsetFolderMenuWindow:NSMakeSize(xOffset, 0.0)];
+ }
+ --displayedButtonCount_;
+ [self reconfigureBookmarkBar];
+ } else if (folderController_) {
+ // 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:YES];
+ }
+}
+
+- (id<BookmarkButtonControllerProtocol>)controllerForNode:
+ (const BookmarkNode*)node {
+ // See if it's in the bar, then if it is in the hierarchy of visible
+ // folder menus.
+ if (bookmarkModel_->GetBookmarkBarNode() == node)
+ return self;
+ return [folderController_ controllerForNode:node];
+}
+
+#pragma mark BookmarkButtonControllerProtocol
+
+// NOT an override of a standard Cocoa call made to NSViewControllers.
+- (void)hookForEvent:(NSEvent*)theEvent {
+ if ([self isEventAnExitEvent:theEvent])
+ [self closeFolderAndStopTrackingMenus];
+}
+
+#pragma mark TestingAPI Only
+
+- (NSMenu*)buttonContextMenu {
+ return buttonContextMenu_;
+}
+
+// Intentionally ignores ownership issues; used for testing and we try
+// to minimize touching the object passed in (likely a mock).
+- (void)setButtonContextMenu:(id)menu {
+ buttonContextMenu_ = menu;
+}
+
+- (void)setIgnoreAnimations:(BOOL)ignore {
+ ignoreAnimations_ = ignore;
+}
+
+@end