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