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