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