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