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