• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
115namespace {
116
117// Overlap (in pixels) between the toolbar and the bookmark bar (when showing in
118// normal mode).
119const CGFloat kBookmarkBarOverlap = 3.0;
120
121// Duration of the bookmark bar animations.
122const NSTimeInterval kBookmarkBarAnimationDuration = 0.12;
123
124void 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.
1819static 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