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