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