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