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/metrics/histogram.h"
9 #include "base/prefs/pref_service.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
12 #include "chrome/browser/bookmarks/bookmark_stats.h"
13 #include "chrome/browser/bookmarks/managed_bookmark_service_factory.h"
14 #include "chrome/browser/prefs/incognito_mode_prefs.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/themes/theme_properties.h"
17 #include "chrome/browser/themes/theme_service.h"
18 #import "chrome/browser/themes/theme_service_factory.h"
19 #include "chrome/browser/ui/bookmarks/bookmark_editor.h"
20 #include "chrome/browser/ui/bookmarks/bookmark_utils.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/browser_list.h"
23 #include "chrome/browser/ui/chrome_pages.h"
24 #import "chrome/browser/ui/cocoa/background_gradient_view.h"
25 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h"
26 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
27 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
28 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h"
29 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_cocoa.h"
30 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
31 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
32 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_context_menu_cocoa_controller.h"
33 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
34 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
35 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
36 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h"
37 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
38 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
39 #import "chrome/browser/ui/cocoa/menu_button.h"
40 #import "chrome/browser/ui/cocoa/presentation_mode_controller.h"
41 #import "chrome/browser/ui/cocoa/themed_window.h"
42 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
43 #import "chrome/browser/ui/cocoa/view_id_util.h"
44 #import "chrome/browser/ui/cocoa/view_resizer.h"
45 #include "chrome/browser/ui/tabs/tab_strip_model.h"
46 #include "chrome/common/extensions/extension_constants.h"
47 #include "chrome/common/extensions/extension_metrics.h"
48 #include "chrome/common/pref_names.h"
49 #include "chrome/common/url_constants.h"
50 #include "chrome/grit/generated_resources.h"
51 #include "components/bookmarks/browser/bookmark_model.h"
52 #include "components/bookmarks/browser/bookmark_node_data.h"
53 #include "components/bookmarks/browser/bookmark_utils.h"
54 #include "components/bookmarks/managed/managed_bookmark_service.h"
55 #include "content/public/browser/user_metrics.h"
56 #include "content/public/browser/web_contents.h"
57 #include "extensions/browser/extension_registry.h"
58 #include "extensions/common/extension.h"
59 #include "extensions/common/extension_set.h"
60 #include "grit/theme_resources.h"
61 #import "ui/base/cocoa/cocoa_base_utils.h"
62 #include "ui/base/l10n/l10n_util_mac.h"
63 #include "ui/base/resource/resource_bundle.h"
64 #include "ui/gfx/image/image.h"
65 #include "ui/resources/grit/ui_resources.h"
67 using base::UserMetricsAction;
68 using bookmarks::BookmarkModel;
69 using bookmarks::BookmarkNode;
70 using bookmarks::BookmarkNodeData;
71 using content::OpenURLParams;
72 using content::Referrer;
73 using content::WebContents;
75 // Bookmark bar state changing and animations
77 // The bookmark bar has three real states: "showing" (a normal bar attached to
78 // the toolbar), "hidden", and "detached" (pretending to be part of the web
79 // content on the NTP). It can, or at least should be able to, animate between
80 // these states. There are several complications even without animation:
81 // - The placement of the bookmark bar is done by the BWC, and it needs to know
82 // the state in order to place the bookmark bar correctly (immediately below
83 // the toolbar when showing, below the infobar when detached).
84 // - The "divider" (a black line) needs to be drawn by either the toolbar (when
85 // the bookmark bar is hidden or detached) or by the bookmark bar (when it is
86 // showing). It should not be drawn by both.
87 // - The toolbar needs to vertically "compress" when the bookmark bar is
88 // showing. This ensures the proper display of both the bookmark bar and the
89 // toolbar, and gives a padded area around the bookmark bar items for right
92 // Our model is that the BWC controls us and also the toolbar. We try not to
93 // talk to the browser nor the toolbar directly, instead centralizing control in
94 // the BWC. The key method by which the BWC controls us is
95 // |-updateState:ChangeType:|. This invokes state changes, and at appropriate
96 // times we request that the BWC do things for us via either the resize delegate
97 // or our general delegate. If the BWC needs any information about what it
98 // should do, or tell the toolbar to do, it can then query us back (e.g.,
99 // |-isShownAs...|, |-getDesiredToolbarHeightCompression|,
100 // |-toolbarDividerOpacity|, etc.).
102 // Animation-related complications:
103 // - Compression of the toolbar is touchy during animation. It must not be
104 // compressed while the bookmark bar is animating to/from showing (from/to
105 // hidden), otherwise it would look like the bookmark bar's contents are
106 // sliding out of the controls inside the toolbar. As such, we have to make
107 // sure that the bookmark bar is shown at the right location and at the
108 // right height (at various points in time).
109 // - Showing the divider is also complicated during animation between hidden
110 // and showing. We have to make sure that the toolbar does not show the
111 // divider despite the fact that it's not compressed. The exception to this
112 // is at the beginning/end of the animation when the toolbar is still
113 // uncompressed but the bookmark bar has height 0. If we're not careful, we
114 // get a flicker at this point.
115 // - We have to ensure that we do the right thing if we're told to change state
116 // while we're running an animation. The generic/easy thing to do is to jump
117 // to the end state of our current animation, and (if the new state change
118 // again involves an animation) begin the new animation. We can do better
119 // than that, however, and sometimes just change the current animation to go
120 // to the new end state (e.g., by "reversing" the animation in the showing ->
121 // hidden -> showing case). We also have to ensure that demands to
122 // immediately change state are always honoured.
124 // Pointers to animation logic:
125 // - |-moveToState:withAnimation:| starts animations, deciding which ones we
126 // know how to handle.
127 // - |-doBookmarkBarAnimation| has most of the actual logic.
128 // - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain
130 // - The BWC's |-layoutSubviews| needs to know how to position things.
131 // - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and
132 // |-bookmarkBar:willAnimateFromState:toState:| in order to inform the
133 // toolbar of required changes.
137 // Duration of the bookmark bar animations.
138 const NSTimeInterval kBookmarkBarAnimationDuration = 0.12;
139 const NSTimeInterval kDragAndDropAnimationDuration = 0.25;
141 void RecordAppLaunch(Profile* profile, GURL url) {
142 const extensions::Extension* extension =
143 extensions::ExtensionRegistry::Get(profile)->
144 enabled_extensions().GetAppByURL(url);
148 extensions::RecordAppLaunchType(extension_misc::APP_LAUNCH_BOOKMARK_BAR,
149 extension->GetType());
154 @interface BookmarkBarController(Private)
156 // Moves to the given next state (from the current state), possibly animating.
157 // If |animate| is NO, it will stop any running animation and jump to the given
158 // state. If YES, it may either (depending on implementation) jump to the end of
159 // the current animation and begin the next one, or stop the current animation
160 // mid-flight and animate to the next state.
161 - (void)moveToState:(BookmarkBar::State)nextState
162 withAnimation:(BOOL)animate;
164 // Return the backdrop to the bookmark bar as various types.
165 - (BackgroundGradientView*)backgroundGradientView;
166 - (AnimatableView*)animatableView;
168 // Create buttons for all items in the given bookmark node tree.
169 // Modifies self->buttons_. Do not add more buttons than will fit on the view.
170 - (void)addNodesToButtonList:(const BookmarkNode*)node;
172 // Create an autoreleased button appropriate for insertion into the bookmark
173 // bar. Update |xOffset| with the offset appropriate for the subsequent button.
174 - (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
175 xOffset:(int*)xOffset;
177 // Find a parent whose button is visible on the bookmark bar.
178 - (BookmarkButton*)bookmarkButtonToPulseForNode:(const BookmarkNode*)node;
180 // Puts stuff into the final state without animating, stopping a running
181 // animation if necessary.
182 - (void)finalizeState;
184 // Stops any current animation in its tracks (midway).
185 - (void)stopCurrentAnimation;
187 // Show/hide the bookmark bar.
188 // if |animate| is YES, the changes are made using the animator; otherwise they
189 // are made immediately.
190 - (void)showBookmarkBarWithAnimation:(BOOL)animate;
192 // Handles animating the resize of the content view. Returns YES if it handled
193 // the animation, NO if not (and hence it should be done instantly).
194 - (BOOL)doBookmarkBarAnimation;
196 // |point| is in the base coordinate system of the destination window;
197 // it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
198 // made and inserted into the new location while leaving the bookmark in
199 // the old location, otherwise move the bookmark by removing from its old
200 // location and inserting into the new location.
201 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
205 // Returns the index in the model for a drag to the location given by
206 // |point|. This is determined by finding the first button before the center
207 // of which |point| falls, scanning left to right. Note that, currently, only
208 // the x-coordinate of |point| is considered. Though not currently implemented,
209 // we may check for errors, in which case this would return negative value;
210 // callers should check for this.
211 - (int)indexForDragToPoint:(NSPoint)point;
213 // Add or remove buttons to/from the bar until it is filled but not overflowed.
214 - (void)redistributeButtonsOnBarAsNeeded;
216 // Determine the nature of the bookmark bar contents based on the number of
217 // buttons showing. If too many then show the off-the-side list, if none
218 // then show the no items label.
219 - (void)reconfigureBookmarkBar;
221 - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu;
222 - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu;
223 - (void)tagEmptyMenu:(NSMenu*)menu;
224 - (void)clearMenuTagMap;
225 - (int)preferredHeight;
226 - (void)addButtonsToView;
227 - (BOOL)setBookmarkButtonVisibility;
228 - (BOOL)setManagedBookmarksButtonVisibility;
229 - (BOOL)setSupervisedBookmarksButtonVisibility;
230 - (BOOL)setOtherBookmarksButtonVisibility;
231 - (BOOL)setAppsPageShortcutButtonVisibility;
232 - (BookmarkButton*)createCustomBookmarkButtonForCell:(NSCell*)cell;
233 - (void)createManagedBookmarksButton;
234 - (void)createSupervisedBookmarksButton;
235 - (void)createOtherBookmarksButton;
236 - (void)createAppsPageShortcutButton;
237 - (void)openAppsPage:(id)sender;
238 - (void)centerNoItemsLabel;
239 - (void)positionRightSideButtons;
240 - (void)watchForExitEvent:(BOOL)watch;
241 - (void)resetAllButtonPositionsWithAnimation:(BOOL)animate;
245 @implementation BookmarkBarController
247 @synthesize currentState = currentState_;
248 @synthesize lastState = lastState_;
249 @synthesize isAnimationRunning = isAnimationRunning_;
250 @synthesize delegate = delegate_;
251 @synthesize stateAnimationsEnabled = stateAnimationsEnabled_;
252 @synthesize innerContentAnimationsEnabled = innerContentAnimationsEnabled_;
254 - (id)initWithBrowser:(Browser*)browser
255 initialWidth:(CGFloat)initialWidth
256 delegate:(id<BookmarkBarControllerDelegate>)delegate
257 resizeDelegate:(id<ViewResizer>)resizeDelegate {
258 if ((self = [super initWithNibName:@"BookmarkBar"
259 bundle:base::mac::FrameworkBundle()])) {
260 currentState_ = BookmarkBar::HIDDEN;
261 lastState_ = BookmarkBar::HIDDEN;
264 initialWidth_ = initialWidth;
265 bookmarkModel_ = BookmarkModelFactory::GetForProfile(browser_->profile());
266 managedBookmarkService_ =
267 ManagedBookmarkServiceFactory::GetForProfile(browser_->profile());
268 buttons_.reset([[NSMutableArray alloc] init]);
269 delegate_ = delegate;
270 resizeDelegate_ = resizeDelegate;
272 [[BookmarkFolderTarget alloc] initWithController:self
273 profile:browser_->profile()]);
275 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
277 rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER).CopyNSImage());
279 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
281 innerContentAnimationsEnabled_ = YES;
282 stateAnimationsEnabled_ = YES;
284 // Register for theme changes, bookmark button pulsing, ...
285 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
286 [defaultCenter addObserver:self
287 selector:@selector(themeDidChangeNotification:)
288 name:kBrowserThemeDidChangeNotification
291 contextMenuController_.reset(
292 [[BookmarkContextMenuCocoaController alloc]
293 initWithBookmarkBarController:self]);
295 // This call triggers an -awakeFromNib, which builds the bar, which might
296 // use |folderImage_| and |contextMenuController_|. Ensure it happens after
297 // |folderImage_| is loaded and |contextMenuController_| is created.
298 [[self animatableView] setResizeDelegate:resizeDelegate];
303 - (Browser*)browser {
307 - (BookmarkContextMenuCocoaController*)menuController {
308 return contextMenuController_.get();
311 - (BookmarkButton*)bookmarkButtonToPulseForNode:(const BookmarkNode*)node {
312 // Find the closest parent that is visible on the bar.
314 // Check if we've reached one of the special buttons. Otherwise, if the next
315 // parent is the boomark bar, find the corresponding button.
316 if ([managedBookmarksButton_ bookmarkNode] == node)
317 return managedBookmarksButton_;
319 if ([supervisedBookmarksButton_ bookmarkNode] == node)
320 return supervisedBookmarksButton_;
322 if ([otherBookmarksButton_ bookmarkNode] == node)
323 return otherBookmarksButton_;
325 if ([offTheSideButton_ bookmarkNode] == node)
326 return offTheSideButton_;
328 if (node->parent() == bookmarkModel_->bookmark_bar_node()) {
329 for (BookmarkButton* button in [self buttons]) {
330 if ([button bookmarkNode] == node) {
331 [button setIsContinuousPulsing:YES];
337 node = node->parent();
343 - (void)startPulsingBookmarkNode:(const BookmarkNode*)node {
344 [self stopPulsingBookmarkNode];
346 pulsingButton_ = [self bookmarkButtonToPulseForNode:node];
350 [pulsingButton_ setIsContinuousPulsing:YES];
351 pulsingBookmarkObserver_.reset(
352 new BookmarkModelObserverForCocoa(bookmarkModel_, ^() {
353 // Stop pulsing if anything happened to the node.
354 [self stopPulsingBookmarkNode];
356 pulsingBookmarkObserver_->StartObservingNode(node);
359 - (void)stopPulsingBookmarkNode {
363 [pulsingButton_ setIsContinuousPulsing:NO];
364 pulsingButton_ = nil;
365 pulsingBookmarkObserver_.reset();
369 [self browserWillBeDestroyed];
373 - (void)browserWillBeDestroyed {
374 // Clear delegate so it doesn't get called during stopAnimation.
375 [[self animatableView] setResizeDelegate:nil];
377 // We better stop any in-flight animation if we're being killed.
378 [[self animatableView] stopAnimation];
380 // Remove our view from its superview so it doesn't attempt to reference
381 // it when the controller is gone.
382 //TODO(dmaclach): Remove -- http://crbug.com/25845
383 [[self view] removeFromSuperview];
385 // Be sure there is no dangling pointer.
386 if ([[self view] respondsToSelector:@selector(setController:)])
387 [[self view] performSelector:@selector(setController:) withObject:nil];
389 // For safety, make sure the buttons can no longer call us.
390 for (BookmarkButton* button in buttons_.get()) {
391 [button setDelegate:nil];
392 [button setTarget:nil];
393 [button setAction:nil];
397 [[NSNotificationCenter defaultCenter] removeObserver:self];
398 [self watchForExitEvent:NO];
402 - (void)awakeFromNib {
403 // We default to NOT open, which means height=0.
404 DCHECK([[self view] isHidden]); // Hidden so it's OK to change.
406 // Set our initial height to zero, since that is what the superview
407 // expects. We will resize ourselves open later if needed.
408 [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)];
410 // Complete init of the "off the side" button, as much as we can.
411 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
412 [offTheSideButton_ setImage:
413 rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_CHEVRONS).ToNSImage()];
414 [offTheSideButton_.draggableButton setDraggable:NO];
415 [offTheSideButton_.draggableButton setActsOnMouseDown:YES];
417 // We are enabled by default.
420 // Remember the original sizes of the 'no items' and 'import bookmarks'
421 // fields to aid in resizing when the window frame changes.
422 originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame];
423 originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame];
425 // To make life happier when the bookmark bar is floating, the chevron is a
426 // child of the button view.
427 [offTheSideButton_ removeFromSuperview];
428 [buttonView_ addSubview:offTheSideButton_];
430 // When resized we may need to add new buttons, or remove them (if
431 // no longer visible), or add/remove the "off the side" menu.
432 [[self view] setPostsFrameChangedNotifications:YES];
433 [[NSNotificationCenter defaultCenter]
435 selector:@selector(frameDidChange)
436 name:NSViewFrameDidChangeNotification
439 // Watch for things going to or from fullscreen.
440 [[NSNotificationCenter defaultCenter]
442 selector:@selector(willEnterOrLeaveFullscreen:)
443 name:kWillEnterFullscreenNotification
445 [[NSNotificationCenter defaultCenter]
447 selector:@selector(willEnterOrLeaveFullscreen:)
448 name:kWillLeaveFullscreenNotification
451 // Don't pass ourself along (as 'self') until our init is completely
452 // done. Thus, this call is (almost) last.
453 bridge_.reset(new BookmarkBarBridge(browser_->profile(), self,
457 // Called by our main view (a BookmarkBarView) when it gets moved to a
458 // window. We perform operations which need to know the relevant
459 // window (e.g. watch for a window close) so they can't be performed
460 // earlier (such as in awakeFromNib).
461 - (void)viewDidMoveToWindow {
462 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
464 // Remove any existing notifications before registering for new ones.
465 [defaultCenter removeObserver:self
466 name:NSWindowWillCloseNotification
468 [defaultCenter removeObserver:self
469 name:NSWindowDidResignMainNotification
472 [defaultCenter addObserver:self
473 selector:@selector(parentWindowWillClose:)
474 name:NSWindowWillCloseNotification
475 object:[[self view] window]];
476 [defaultCenter addObserver:self
477 selector:@selector(parentWindowDidResignMain:)
478 name:NSWindowDidResignMainNotification
479 object:[[self view] window]];
482 // When going fullscreen we can run into trouble. Our view is removed
483 // from the non-fullscreen window before the non-fullscreen window
484 // loses key, so our parentDidResignKey: callback never gets called.
485 // In addition, a bookmark folder controller needs to be autoreleased
486 // (in case it's in the event chain when closed), but the release
487 // implicitly needs to happen while it's connected to the original
488 // (non-fullscreen) window to "unlock bar visibility". Such a
489 // contract isn't honored when going fullscreen with the menu option
490 // (not with the keyboard shortcut). We fake it as best we can here.
491 // We have a similar problem leaving fullscreen.
492 - (void)willEnterOrLeaveFullscreen:(NSNotification*)notification {
493 if (folderController_) {
494 [self childFolderWillClose:folderController_];
495 [self closeFolderAndStopTrackingMenus];
499 // NSNotificationCenter callback.
500 - (void)parentWindowWillClose:(NSNotification*)notification {
501 [self closeFolderAndStopTrackingMenus];
504 // NSNotificationCenter callback.
505 - (void)parentWindowDidResignMain:(NSNotification*)notification {
506 [self closeFolderAndStopTrackingMenus];
509 // Change the layout of the bookmark bar's subviews in response to a visibility
510 // change (e.g., show or hide the bar) or style change (attached or floating).
511 - (void)layoutSubviews {
512 NSRect frame = [[self view] frame];
513 NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame));
515 // Add padding to the detached bookmark bar.
516 // The state of our morph (if any); 1 is total bubble, 0 is the regular bar.
517 CGFloat morph = [self detachedMorphProgress];
518 CGFloat padding = bookmarks::kNTPBookmarkBarPadding;
520 NSInsetRect(buttonViewFrame, morph * padding, morph * padding);
522 [buttonView_ setFrame:buttonViewFrame];
524 // Update bookmark button backgrounds.
525 if ([self isAnimationRunning]) {
526 for (NSButton* button in buttons_.get())
527 [button setNeedsDisplay:YES];
528 // Update the apps and other buttons explicitly, since they are not in the
530 [appsPageShortcutButton_ setNeedsDisplay:YES];
531 [managedBookmarksButton_ setNeedsDisplay:YES];
532 [supervisedBookmarksButton_ setNeedsDisplay:YES];
533 [otherBookmarksButton_ setNeedsDisplay:YES];
537 // We don't change a preference; we only change visibility. Preference changing
538 // (global state) is handled in |chrome::ToggleBookmarkBarWhenVisible()|. We
539 // simply update based on what we're told.
540 - (void)updateVisibility {
541 [self showBookmarkBarWithAnimation:NO];
544 - (void)updateExtraButtonsVisibility {
545 if (!appsPageShortcutButton_.get() ||
546 !managedBookmarksButton_.get() ||
547 !supervisedBookmarksButton_.get()) {
550 [self setAppsPageShortcutButtonVisibility];
551 [self setManagedBookmarksButtonVisibility];
552 [self setSupervisedBookmarksButtonVisibility];
553 [self resetAllButtonPositionsWithAnimation:NO];
554 [self reconfigureBookmarkBar];
557 - (void)updateHiddenState {
558 BOOL oldHidden = [[self view] isHidden];
559 BOOL newHidden = ![self isVisible];
560 if (oldHidden != newHidden)
561 [[self view] setHidden:newHidden];
564 - (void)setBookmarkBarEnabled:(BOOL)enabled {
565 if (enabled != barIsEnabled_) {
566 barIsEnabled_ = enabled;
567 [self updateVisibility];
571 - (CGFloat)getDesiredToolbarHeightCompression {
572 // Some special cases....
576 if ([self isAnimationRunning]) {
577 // No toolbar compression when animating between hidden and showing, nor
578 // between showing and detached.
579 if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN
580 andState:BookmarkBar::SHOW] ||
581 [self isAnimatingBetweenState:BookmarkBar::SHOW
582 andState:BookmarkBar::DETACHED])
585 // If we ever need any other animation cases, code would go here.
588 return [self isInState:BookmarkBar::SHOW] ? bookmarks::kBookmarkBarOverlap
592 - (CGFloat)toolbarDividerOpacity {
593 // Some special cases....
594 if ([self isAnimationRunning]) {
595 // In general, the toolbar shouldn't show a divider while we're animating
596 // between showing and hidden. The exception is when our height is < 1, in
597 // which case we can't draw it. It's all-or-nothing (no partial opacity).
598 if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN
599 andState:BookmarkBar::SHOW])
600 return (NSHeight([[self view] frame]) < 1) ? 1 : 0;
602 // The toolbar should show the divider when animating between showing and
603 // detached (but opacity will vary).
604 if ([self isAnimatingBetweenState:BookmarkBar::SHOW
605 andState:BookmarkBar::DETACHED])
606 return static_cast<CGFloat>([self detachedMorphProgress]);
608 // If we ever need any other animation cases, code would go here.
611 // In general, only show the divider when it's in the normal showing state.
612 return [self isInState:BookmarkBar::SHOW] ? 0 : 1;
615 - (NSImage*)faviconForNode:(const BookmarkNode*)node {
617 return defaultImage_;
619 if (node == managedBookmarkService_->managed_node()) {
620 // Most users never see this node, so the image is only loaded if needed.
621 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
622 return rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER_MANAGED).ToNSImage();
625 if (node == managedBookmarkService_->supervised_node()) {
626 // Most users never see this node, so the image is only loaded if needed.
627 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
628 return rb.GetNativeImageNamed(
629 IDR_BOOKMARK_BAR_FOLDER_SUPERVISED).ToNSImage();
632 if (node->is_folder())
635 const gfx::Image& favicon = bookmarkModel_->GetFavicon(node);
636 if (!favicon.IsEmpty())
637 return favicon.ToNSImage();
639 return defaultImage_;
642 - (void)closeFolderAndStopTrackingMenus {
643 showFolderMenus_ = NO;
644 [self closeAllBookmarkFolders];
647 - (BOOL)canEditBookmarks {
648 PrefService* prefs = browser_->profile()->GetPrefs();
649 return prefs->GetBoolean(bookmarks::prefs::kEditBookmarksEnabled);
652 - (BOOL)canEditBookmark:(const BookmarkNode*)node {
653 // Don't allow edit/delete of the permanent nodes.
654 if (node == nil || bookmarkModel_->is_permanent_node(node) ||
655 !managedBookmarkService_->CanBeEditedByUser(node)) {
663 // Helper methods called on the main thread by runMenuFlashThread.
665 - (void)setButtonFlashStateOn:(id)sender {
666 [sender highlight:YES];
669 - (void)setButtonFlashStateOff:(id)sender {
670 [sender highlight:NO];
673 - (void)cleanupAfterMenuFlashThread:(id)sender {
674 [self closeFolderAndStopTrackingMenus];
676 // Items retained by doMenuFlashOnSeparateThread below.
681 // End runMenuFlashThread helper methods.
683 // This call is invoked only by doMenuFlashOnSeparateThread below.
684 // It makes the selected BookmarkButton (which is masquerading as a menu item)
685 // flash a few times to give confirmation feedback, then it closes the menu.
686 // It spends all its time sleeping or scheduling UI work on the main thread.
687 - (void)runMenuFlashThread:(id)sender {
689 // Check this is not running on the main thread, as it sleeps.
690 DCHECK(![NSThread isMainThread]);
692 // Duration of flash phases and number of flashes designed to evoke a
693 // slightly retro "more mac-like than the Mac" feel.
694 // Current Cocoa UI has a barely perceptible flash,probably because Apple
695 // doesn't fire the action til after the animation and so there's a hurry.
696 // As this code is fully asynchronous, it can take its time.
697 const float kBBOnFlashTime = 0.08;
698 const float kBBOffFlashTime = 0.08;
699 const int kBookmarkButtonMenuFlashes = 3;
701 for (int count = 0 ; count < kBookmarkButtonMenuFlashes ; count++) {
702 [self performSelectorOnMainThread:@selector(setButtonFlashStateOn:)
705 [NSThread sleepForTimeInterval:kBBOnFlashTime];
706 [self performSelectorOnMainThread:@selector(setButtonFlashStateOff:)
709 [NSThread sleepForTimeInterval:kBBOffFlashTime];
711 [self performSelectorOnMainThread:@selector(cleanupAfterMenuFlashThread:)
716 // Non-blocking call which starts the process to make the selected menu item
717 // flash a few times to give confirmation feedback, after which it closes the
718 // menu. The item is of course actually a BookmarkButton masquerading as a menu
720 - (void)doMenuFlashOnSeparateThread:(id)sender {
722 // Ensure that self and sender don't go away before the animation completes.
723 // These retains are balanced in cleanupAfterMenuFlashThread above.
726 [NSThread detachNewThreadSelector:@selector(runMenuFlashThread:)
731 - (IBAction)openBookmark:(id)sender {
732 BOOL isMenuItem = [[sender cell] isFolderButtonCell];
733 BOOL animate = isMenuItem && innerContentAnimationsEnabled_;
735 [self doMenuFlashOnSeparateThread:sender];
736 DCHECK([sender respondsToSelector:@selector(bookmarkNode)]);
737 const BookmarkNode* node = [sender bookmarkNode];
739 WindowOpenDisposition disposition =
740 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
741 RecordAppLaunch(browser_->profile(), node->url());
742 [self openURL:node->url() disposition:disposition];
745 [self closeFolderAndStopTrackingMenus];
746 RecordBookmarkLaunch(node, [self bookmarkLaunchLocation]);
749 // Common function to open a bookmark folder of any type.
750 - (void)openBookmarkFolder:(id)sender {
751 DCHECK([sender isKindOfClass:[BookmarkButton class]]);
752 DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]);
754 // Only record the action if it's the initial folder being opened.
755 if (!showFolderMenus_)
756 RecordBookmarkFolderOpen([self bookmarkLaunchLocation]);
757 showFolderMenus_ = !showFolderMenus_;
759 // Middle click on chevron should not open bookmarks under it, instead just
760 // open its folder menu.
761 if (sender == offTheSideButton_) {
762 [[sender cell] setStartingChildIndex:displayedButtonCount_];
763 NSEvent* event = [NSApp currentEvent];
764 if ([event type] == NSOtherMouseUp) {
765 [self openOrCloseBookmarkFolderForOffTheSideButton];
769 // Toggle presentation of bar folder menus.
770 [folderTarget_ openBookmarkFolderFromButton:sender];
773 - (void)openOrCloseBookmarkFolderForOffTheSideButton {
774 // If clicked on already opened folder, then close it and return.
775 if ([folderController_ parentButton] == offTheSideButton_)
776 [self closeBookmarkFolder:self];
778 [self addNewFolderControllerWithParentButton:offTheSideButton_];
781 // Click on a bookmark folder button.
782 - (IBAction)openBookmarkFolderFromButton:(id)sender {
783 [self openBookmarkFolder:sender];
786 // Click on the "off the side" button (chevron), which opens like a folder
787 // button but isn't exactly a parent folder.
788 - (IBAction)openOffTheSideFolderFromButton:(id)sender {
789 [self openBookmarkFolder:sender];
792 - (IBAction)importBookmarks:(id)sender {
793 chrome::ShowImportDialog(browser_);
796 #pragma mark Private Methods
798 // Called after a theme change took place, possibly for a different profile.
799 - (void)themeDidChangeNotification:(NSNotification*)notification {
800 [self updateTheme:[[[self view] window] themeProvider]];
803 // (Private) Method is the same as [self view], but is provided to be explicit.
804 - (BackgroundGradientView*)backgroundGradientView {
805 DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]);
806 return (BackgroundGradientView*)[self view];
809 // (Private) Method is the same as [self view], but is provided to be explicit.
810 - (AnimatableView*)animatableView {
811 DCHECK([[self view] isKindOfClass:[AnimatableView class]]);
812 return (AnimatableView*)[self view];
815 - (BookmarkLaunchLocation)bookmarkLaunchLocation {
816 return currentState_ == BookmarkBar::DETACHED ?
817 BOOKMARK_LAUNCH_LOCATION_DETACHED_BAR :
818 BOOKMARK_LAUNCH_LOCATION_ATTACHED_BAR;
821 // Position the right-side buttons including the off-the-side chevron.
822 - (void)positionRightSideButtons {
823 int maxX = NSMaxX([[self buttonView] bounds]) -
824 bookmarks::kBookmarkHorizontalPadding;
828 NSRect frame = [self frameForBookmarkButtonFromCell:
829 [otherBookmarksButton_ cell] xOffset:&ignored];
830 if (![otherBookmarksButton_ isHidden]) {
831 right -= NSWidth(frame);
832 frame.origin.x = right;
834 frame.origin.x = maxX - NSWidth(frame);
836 [otherBookmarksButton_ setFrame:frame];
838 frame = [offTheSideButton_ frame];
839 frame.size.height = bookmarks::kBookmarkFolderButtonHeight;
840 right -= frame.size.width;
841 frame.origin.x = right;
842 [offTheSideButton_ setFrame:frame];
845 // Configure the off-the-side button (e.g. specify the node range,
846 // check if we should enable or disable it, etc).
847 - (void)configureOffTheSideButtonContentsAndVisibility {
848 [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_];
849 [[offTheSideButton_ cell]
850 setBookmarkNode:bookmarkModel_->bookmark_bar_node()];
851 int bookmarkChildren = bookmarkModel_->bookmark_bar_node()->child_count();
852 if (bookmarkChildren > displayedButtonCount_) {
853 [offTheSideButton_ setHidden:NO];
855 // If we just deleted the last item in an off-the-side menu so the
856 // button will be going away, make sure the menu goes away.
857 if (folderController_ &&
858 ([folderController_ parentButton] == offTheSideButton_))
859 [self closeAllBookmarkFolders];
860 // (And hide the button, too.)
861 [offTheSideButton_ setHidden:YES];
865 // Main menubar observation code, so we can know to close our fake menus if the
866 // user clicks on the actual menubar, as multiple unconnected menus sharing
867 // the screen looks weird.
868 // Needed because the local event monitor doesn't see the click on the menubar.
870 // Gets called when the menubar is clicked.
871 - (void)begunTracking:(NSNotification *)notification {
872 [self closeFolderAndStopTrackingMenus];
875 // Install the callback.
876 - (void)startObservingMenubar {
877 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
879 selector:@selector(begunTracking:)
880 name:NSMenuDidBeginTrackingNotification
881 object:[NSApp mainMenu]];
884 // Remove the callback.
885 - (void)stopObservingMenubar {
886 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
887 [nc removeObserver:self
888 name:NSMenuDidBeginTrackingNotification
889 object:[NSApp mainMenu]];
892 // End of menubar observation code.
894 // Begin (or end) watching for a click outside this window. Unlike
895 // normal NSWindows, bookmark folder "fake menu" windows do not become
896 // key or main. Thus, traditional notification (e.g. WillResignKey)
897 // won't work. Our strategy is to watch (at the app level) for a
898 // "click outside" these windows to detect when they logically lose
900 - (void)watchForExitEvent:(BOOL)watch {
902 if (!exitEventTap_) {
903 exitEventTap_ = [NSEvent
904 addLocalMonitorForEventsMatchingMask:NSAnyEventMask
905 handler:^NSEvent* (NSEvent* event) {
906 if ([self isEventAnExitEvent:event])
907 [self closeFolderAndStopTrackingMenus];
910 [self startObservingMenubar];
914 [NSEvent removeMonitor:exitEventTap_];
916 [self stopObservingMenubar];
921 // Keep the "no items" label centered in response to a frame size change.
922 - (void)centerNoItemsLabel {
923 // Note that this computation is done in the parent's coordinate system,
924 // which is unflipped. Also, we want the label to be a fixed distance from
925 // the bottom, so that it slides up properly (on animating to hidden).
926 // The textfield sits in the itemcontainer, so to center it we maintain
927 // equal vertical padding on the top and bottom.
928 int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) -
929 NSHeight([[buttonView_ noItemContainer] frame])) / 2;
930 [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)];
934 - (void)showBookmarkBarWithAnimation:(BOOL)animate {
935 if (animate && stateAnimationsEnabled_) {
936 // If |-doBookmarkBarAnimation| does the animation, we're done.
937 if ([self doBookmarkBarAnimation])
940 // Else fall through and do the change instantly.
944 [resizeDelegate_ resizeView:[self view]
945 newHeight:[self preferredHeight]];
947 // Only show the divider if showing the normal bookmark bar.
948 BOOL showsDivider = [self isInState:BookmarkBar::SHOW];
949 [[self backgroundGradientView] setShowsDivider:showsDivider];
951 // Make sure we're shown.
952 [[self view] setHidden:![self isVisible]];
954 // Update everything else.
955 [self layoutSubviews];
956 [self frameDidChange];
960 - (BOOL)doBookmarkBarAnimation {
961 if ([self isAnimatingFromState:BookmarkBar::HIDDEN
962 toState:BookmarkBar::SHOW]) {
963 [[self backgroundGradientView] setShowsDivider:YES];
964 [[self view] setHidden:NO];
965 AnimatableView* view = [self animatableView];
966 // Height takes into account the extra height we have since the toolbar
967 // only compresses when we're done.
968 [view animateToNewHeight:(chrome::kBookmarkBarHeight -
969 bookmarks::kBookmarkBarOverlap)
970 duration:kBookmarkBarAnimationDuration];
971 } else if ([self isAnimatingFromState:BookmarkBar::SHOW
972 toState:BookmarkBar::HIDDEN]) {
973 [[self backgroundGradientView] setShowsDivider:YES];
974 [[self view] setHidden:NO];
975 AnimatableView* view = [self animatableView];
976 [view animateToNewHeight:0
977 duration:kBookmarkBarAnimationDuration];
978 } else if ([self isAnimatingFromState:BookmarkBar::SHOW
979 toState:BookmarkBar::DETACHED]) {
980 [[self backgroundGradientView] setShowsDivider:YES];
981 [[self view] setHidden:NO];
982 AnimatableView* view = [self animatableView];
983 [view animateToNewHeight:chrome::kNTPBookmarkBarHeight
984 duration:kBookmarkBarAnimationDuration];
985 } else if ([self isAnimatingFromState:BookmarkBar::DETACHED
986 toState:BookmarkBar::SHOW]) {
987 [[self backgroundGradientView] setShowsDivider:YES];
988 [[self view] setHidden:NO];
989 AnimatableView* view = [self animatableView];
990 // Height takes into account the extra height we have since the toolbar
991 // only compresses when we're done.
992 [view animateToNewHeight:(chrome::kBookmarkBarHeight -
993 bookmarks::kBookmarkBarOverlap)
994 duration:kBookmarkBarAnimationDuration];
996 // Oops! An animation we don't know how to handle.
1003 // Actually open the URL. This is the last chance for a unit test to
1005 - (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition {
1006 OpenURLParams params(
1007 url, Referrer(), disposition, ui::PAGE_TRANSITION_AUTO_BOOKMARK,
1009 browser_->OpenURL(params);
1012 - (void)clearMenuTagMap {
1014 menuTagMap_.clear();
1017 - (int)preferredHeight {
1018 DCHECK(![self isAnimationRunning]);
1023 switch (currentState_) {
1024 case BookmarkBar::SHOW:
1025 return chrome::kBookmarkBarHeight;
1026 case BookmarkBar::DETACHED:
1027 return chrome::kNTPBookmarkBarHeight;
1028 case BookmarkBar::HIDDEN:
1033 // Recursively add the given bookmark node and all its children to
1034 // menu, one menu item per node.
1035 - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu {
1036 NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child];
1037 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title
1039 keyEquivalent:@""] autorelease];
1040 [menu addItem:item];
1041 [item setImage:[self faviconForNode:child]];
1042 if (child->is_folder()) {
1043 NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
1044 [menu setSubmenu:submenu forItem:item];
1045 if (!child->empty()) {
1046 [self addFolderNode:child toMenu:submenu]; // potentially recursive
1048 [self tagEmptyMenu:submenu];
1051 [item setTarget:self];
1052 [item setAction:@selector(openBookmarkMenuItem:)];
1053 [item setTag:[self menuTagFromNodeId:child->id()]];
1054 if (child->is_url())
1055 [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:child]];
1059 // Empty menus are odd; if empty, add something to look at.
1060 // Matches windows behavior.
1061 - (void)tagEmptyMenu:(NSMenu*)menu {
1062 NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
1063 [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title
1065 keyEquivalent:@""] autorelease]];
1068 // Add the children of the given bookmark node (and their children...)
1069 // to menu, one menu item per node.
1070 - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu {
1071 for (int i = 0; i < node->child_count(); i++) {
1072 const BookmarkNode* child = node->GetChild(i);
1073 [self addNode:child toMenu:menu];
1077 // Return an autoreleased NSMenu that represents the given bookmark
1079 - (NSMenu *)menuForFolderNode:(const BookmarkNode*)node {
1080 if (!node->is_folder())
1082 NSString* title = base::SysUTF16ToNSString(node->GetTitle());
1083 NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease];
1084 [self addFolderNode:node toMenu:menu];
1086 if (![menu numberOfItems]) {
1087 [self tagEmptyMenu:menu];
1092 // Return an appropriate width for the given bookmark button cell.
1093 // The "+2" is needed because, sometimes, Cocoa is off by a tad.
1094 // Example: for a bookmark named "Moma" or "SFGate", it is one pixel
1095 // too small. For "FBL" it is 2 pixels too small.
1096 // For a bookmark named "SFGateFooWoo", it is just fine.
1097 - (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell {
1098 CGFloat desired = [cell cellSize].width + 2;
1099 return std::min(desired, bookmarks::kDefaultBookmarkWidth);
1102 - (IBAction)openBookmarkMenuItem:(id)sender {
1103 int64 tag = [self nodeIdFromMenuTag:[sender tag]];
1104 const BookmarkNode* node =
1105 bookmarks::GetBookmarkNodeByID(bookmarkModel_, tag);
1106 WindowOpenDisposition disposition =
1107 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
1108 [self openURL:node->url() disposition:disposition];
1111 // For the given root node of the bookmark bar, show or hide (as
1112 // appropriate) the "no items" container (text which says "bookmarks
1114 - (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node {
1115 BOOL hideNoItemWarning = !node->empty();
1116 [[buttonView_ noItemContainer] setHidden:hideNoItemWarning];
1119 // TODO(jrg): write a "build bar" so there is a nice spot for things
1120 // like the contextual menu which is invoked when not over a
1121 // bookmark. On Safari that menu has a "new folder" option.
1122 - (void)addNodesToButtonList:(const BookmarkNode*)node {
1123 [self showOrHideNoItemContainerForNode:node];
1125 CGFloat maxViewX = NSMaxX([[self view] bounds]);
1127 bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding;
1129 // Draw the apps bookmark if needed.
1130 if (![appsPageShortcutButton_ isHidden]) {
1132 [self frameForBookmarkButtonFromCell:[appsPageShortcutButton_ cell]
1134 [appsPageShortcutButton_ setFrame:frame];
1137 // Draw the managed bookmark folder if needed.
1138 if (![managedBookmarksButton_ isHidden]) {
1139 xOffset += bookmarks::kBookmarkHorizontalPadding;
1141 [self frameForBookmarkButtonFromCell:[managedBookmarksButton_ cell]
1143 [managedBookmarksButton_ setFrame:frame];
1146 // Draw the supervised bookmark folder if needed.
1147 if (![supervisedBookmarksButton_ isHidden]) {
1148 xOffset += bookmarks::kBookmarkHorizontalPadding;
1150 [self frameForBookmarkButtonFromCell:[supervisedBookmarksButton_ cell]
1152 [supervisedBookmarksButton_ setFrame:frame];
1155 for (int i = 0; i < node->child_count(); i++) {
1156 const BookmarkNode* child = node->GetChild(i);
1157 BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
1158 if (NSMinX([button frame]) >= maxViewX) {
1159 [button setDelegate:nil];
1162 [buttons_ addObject:button];
1166 - (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
1167 xOffset:(int*)xOffset {
1168 BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
1169 NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset];
1171 base::scoped_nsobject<BookmarkButton> button(
1172 [[BookmarkButton alloc] initWithFrame:frame]);
1173 DCHECK(button.get());
1175 // [NSButton setCell:] warns to NOT use setCell: other than in the
1176 // initializer of a control. However, we are using a basic
1177 // NSButton whose initializer does not take an NSCell as an
1178 // object. To honor the assumed semantics, we do nothing with
1179 // NSButton between alloc/init and setCell:.
1180 [button setCell:cell];
1181 [button setDelegate:self];
1183 // We cannot set the button cell's text color until it is placed in
1184 // the button (e.g. the [button setCell:cell] call right above). We
1185 // also cannot set the cell's text color until the view is added to
1186 // the hierarchy. If that second part is now true, set the color.
1187 // (If not we'll set the color on the 1st themeChanged:
1189 ui::ThemeProvider* themeProvider = [[[self view] window] themeProvider];
1190 if (themeProvider) {
1192 themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
1193 [cell setTextColor:color];
1196 if (node->is_folder()) {
1197 [button setTarget:self];
1198 [button setAction:@selector(openBookmarkFolderFromButton:)];
1199 [[button draggableButton] setActsOnMouseDown:YES];
1200 // If it has a title, and it will be truncated, show full title in
1202 NSString* title = base::SysUTF16ToNSString(node->GetTitle());
1203 if ([title length] &&
1204 [[button cell] cellSize].width > bookmarks::kDefaultBookmarkWidth) {
1205 [button setToolTip:title];
1208 // Make the button do something
1209 [button setTarget:self];
1210 [button setAction:@selector(openBookmark:)];
1212 [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
1214 return [[button.get() retain] autorelease];
1217 // Add bookmark buttons to the view only if they are completely
1218 // visible and don't overlap the "other bookmarks". Remove buttons
1219 // which are clipped. Called when building the bookmark bar the first time.
1220 - (void)addButtonsToView {
1221 displayedButtonCount_ = 0;
1222 NSMutableArray* buttons = [self buttons];
1223 for (NSButton* button in buttons) {
1224 if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) -
1225 bookmarks::kBookmarkHorizontalPadding))
1227 [buttonView_ addSubview:button];
1228 ++displayedButtonCount_;
1230 NSUInteger removalCount =
1231 [buttons count] - (NSUInteger)displayedButtonCount_;
1232 if (removalCount > 0) {
1233 NSRange removalRange = NSMakeRange(displayedButtonCount_, removalCount);
1234 [buttons removeObjectsInRange:removalRange];
1238 // Shows or hides the Managed, Supervised, or Other Bookmarks button as
1239 // appropriate, and returns whether it ended up visible.
1240 - (BOOL)setBookmarkButtonVisibility:(BookmarkButton*)button
1242 resetAllButtonPositions:(BOOL)resetButtons {
1246 BOOL visible = ![button bookmarkNode]->empty() && show;
1247 BOOL currentVisibility = ![button isHidden];
1248 if (currentVisibility != visible) {
1249 [button setHidden:!visible];
1251 [self resetAllButtonPositionsWithAnimation:NO];
1256 // Shows or hides the Managed Bookmarks button as appropriate, and returns
1257 // whether it ended up visible.
1258 - (BOOL)setManagedBookmarksButtonVisibility {
1259 PrefService* prefs = browser_->profile()->GetPrefs();
1261 prefs->GetBoolean(bookmarks::prefs::kShowManagedBookmarksInBookmarkBar);
1262 return [self setBookmarkButtonVisibility:managedBookmarksButton_.get()
1264 resetAllButtonPositions:YES];
1267 // Shows or hides the Supervised Bookmarks button as appropriate, and returns
1268 // whether it ended up visible.
1269 - (BOOL)setSupervisedBookmarksButtonVisibility {
1270 return [self setBookmarkButtonVisibility:supervisedBookmarksButton_.get()
1272 resetAllButtonPositions:YES];
1275 // Shows or hides the Other Bookmarks button as appropriate, and returns
1276 // whether it ended up visible.
1277 - (BOOL)setOtherBookmarksButtonVisibility {
1278 return [self setBookmarkButtonVisibility:otherBookmarksButton_.get()
1280 resetAllButtonPositions:NO];
1283 // Shows or hides the Apps button as appropriate, and returns whether it ended
1285 - (BOOL)setAppsPageShortcutButtonVisibility {
1286 if (!appsPageShortcutButton_.get())
1289 BOOL visible = bookmarkModel_->loaded() &&
1290 chrome::ShouldShowAppsShortcutInBookmarkBar(
1291 browser_->profile(), browser_->host_desktop_type());
1292 [appsPageShortcutButton_ setHidden:!visible];
1296 // Creates a bookmark bar button that does not correspond to a regular bookmark
1297 // or folder. It is used by the "Other Bookmarks" and the "Apps" buttons.
1298 - (BookmarkButton*)createCustomBookmarkButtonForCell:(NSCell*)cell {
1299 BookmarkButton* button = [[BookmarkButton alloc] init];
1300 [[button draggableButton] setDraggable:NO];
1301 [[button draggableButton] setActsOnMouseDown:YES];
1302 [button setCell:cell];
1303 [button setDelegate:self];
1304 [button setTarget:self];
1305 // Make sure this button, like all other BookmarkButtons, lives
1306 // until the end of the current event loop.
1307 [[button retain] autorelease];
1311 // Creates the button for "Managed Bookmarks", but does not position it.
1312 - (void)createManagedBookmarksButton {
1313 if (managedBookmarksButton_.get()) {
1314 // The node's title might have changed if the user signed in or out.
1315 // Make sure it's up to date now.
1316 const BookmarkNode* node = managedBookmarkService_->managed_node();
1317 NSString* title = base::SysUTF16ToNSString(node->GetTitle());
1318 NSCell* cell = [managedBookmarksButton_ cell];
1319 [cell setTitle:title];
1321 // Its visibility may have changed too.
1322 [self setManagedBookmarksButtonVisibility];
1328 [self cellForBookmarkNode:managedBookmarkService_->managed_node()];
1329 managedBookmarksButton_.reset([self createCustomBookmarkButtonForCell:cell]);
1330 [managedBookmarksButton_ setAction:@selector(openBookmarkFolderFromButton:)];
1331 view_id_util::SetID(managedBookmarksButton_.get(), VIEW_ID_MANAGED_BOOKMARKS);
1332 [buttonView_ addSubview:managedBookmarksButton_.get()];
1334 [self setManagedBookmarksButtonVisibility];
1337 // Creates the button for "Supervised Bookmarks", but does not position it.
1338 - (void)createSupervisedBookmarksButton {
1339 if (supervisedBookmarksButton_.get()) {
1340 // The button's already there, but its visibility may have changed.
1341 [self setSupervisedBookmarksButtonVisibility];
1346 [self cellForBookmarkNode:managedBookmarkService_->supervised_node()];
1347 supervisedBookmarksButton_.reset(
1348 [self createCustomBookmarkButtonForCell:cell]);
1349 [supervisedBookmarksButton_
1350 setAction:@selector(openBookmarkFolderFromButton:)];
1351 view_id_util::SetID(supervisedBookmarksButton_.get(),
1352 VIEW_ID_SUPERVISED_BOOKMARKS);
1353 [buttonView_ addSubview:supervisedBookmarksButton_.get()];
1355 [self setSupervisedBookmarksButtonVisibility];
1358 // Creates the button for "Other Bookmarks", but does not position it.
1359 - (void)createOtherBookmarksButton {
1360 // Can't create this until the model is loaded, but only need to
1362 if (otherBookmarksButton_.get()) {
1363 [self setOtherBookmarksButtonVisibility];
1367 NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()];
1368 otherBookmarksButton_.reset([self createCustomBookmarkButtonForCell:cell]);
1369 // Peg at right; keep same height as bar.
1370 [otherBookmarksButton_ setAutoresizingMask:(NSViewMinXMargin)];
1371 [otherBookmarksButton_ setAction:@selector(openBookmarkFolderFromButton:)];
1372 view_id_util::SetID(otherBookmarksButton_.get(), VIEW_ID_OTHER_BOOKMARKS);
1373 [buttonView_ addSubview:otherBookmarksButton_.get()];
1375 [self setOtherBookmarksButtonVisibility];
1378 // Creates the button for "Apps", but does not position it.
1379 - (void)createAppsPageShortcutButton {
1380 // Can't create this until the model is loaded, but only need to
1382 if (appsPageShortcutButton_.get()) {
1383 [self setAppsPageShortcutButtonVisibility];
1387 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
1388 NSString* text = l10n_util::GetNSString(IDS_BOOKMARK_BAR_APPS_SHORTCUT_NAME);
1389 NSImage* image = rb.GetNativeImageNamed(
1390 IDR_BOOKMARK_BAR_APPS_SHORTCUT).ToNSImage();
1391 NSCell* cell = [self cellForCustomButtonWithText:text
1393 appsPageShortcutButton_.reset([self createCustomBookmarkButtonForCell:cell]);
1394 [[appsPageShortcutButton_ draggableButton] setActsOnMouseDown:NO];
1395 [appsPageShortcutButton_ setAction:@selector(openAppsPage:)];
1397 l10n_util::GetNSString(IDS_BOOKMARK_BAR_APPS_SHORTCUT_TOOLTIP);
1398 [appsPageShortcutButton_ setToolTip:tooltip];
1399 [buttonView_ addSubview:appsPageShortcutButton_.get()];
1401 [self setAppsPageShortcutButtonVisibility];
1404 - (void)openAppsPage:(id)sender {
1405 WindowOpenDisposition disposition =
1406 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
1407 [self openURL:GURL(chrome::kChromeUIAppsURL) disposition:disposition];
1408 RecordBookmarkAppsPageOpen([self bookmarkLaunchLocation]);
1411 // To avoid problems with sync, changes that may impact the current
1412 // bookmark (e.g. deletion) make sure context menus are closed. This
1413 // prevents deleting a node which no longer exists.
1414 - (void)cancelMenuTracking {
1415 [contextMenuController_ cancelTracking];
1418 - (void)moveToState:(BookmarkBar::State)nextState
1419 withAnimation:(BOOL)animate {
1420 BOOL isAnimationRunning = [self isAnimationRunning];
1422 // No-op if the next state is the same as the "current" one, subject to the
1423 // following conditions:
1424 // - no animation is running; or
1425 // - an animation is running and |animate| is YES ([*] if it's NO, we'd want
1426 // to cancel the animation and jump to the final state).
1427 if ((nextState == currentState_) && (!isAnimationRunning || animate))
1430 // If an animation is running, we want to finalize it. Otherwise we'd have to
1431 // be able to animate starting from the middle of one type of animation. We
1432 // assume that animations that we know about can be "reversed".
1433 if (isAnimationRunning) {
1434 // Don't cancel if we're going to reverse the animation.
1435 if (nextState != lastState_) {
1436 [self stopCurrentAnimation];
1437 [self finalizeState];
1440 // If we're in case [*] above, we can stop here.
1441 if (nextState == currentState_)
1445 // Now update with the new state change.
1446 lastState_ = currentState_;
1447 currentState_ = nextState;
1448 isAnimationRunning_ = YES;
1450 // Animate only if told to and if bar is enabled.
1451 if (animate && stateAnimationsEnabled_ && barIsEnabled_) {
1452 [self closeAllBookmarkFolders];
1453 // Take care of any animation cases we know how to handle.
1455 // We know how to handle hidden <-> normal, normal <-> detached....
1456 if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN
1457 andState:BookmarkBar::SHOW] ||
1458 [self isAnimatingBetweenState:BookmarkBar::SHOW
1459 andState:BookmarkBar::DETACHED]) {
1460 [delegate_ bookmarkBar:self
1461 willAnimateFromState:lastState_
1462 toState:currentState_];
1463 [self showBookmarkBarWithAnimation:YES];
1467 // If we ever need any other animation cases, code would go here.
1468 // Let any animation cases which we don't know how to handle fall through to
1469 // the unanimated case.
1472 // Just jump to the state.
1473 [self finalizeState];
1476 // N.B.: |-moveToState:...| will check if this should be a no-op or not.
1477 - (void)updateState:(BookmarkBar::State)newState
1478 changeType:(BookmarkBar::AnimateChangeType)changeType {
1479 BOOL animate = changeType == BookmarkBar::ANIMATE_STATE_CHANGE &&
1480 stateAnimationsEnabled_;
1481 [self moveToState:newState withAnimation:animate];
1485 - (void)finalizeState {
1486 // We promise that our delegate that the variables will be finalized before
1487 // the call to |-bookmarkBar:didChangeFromState:toState:|.
1488 BookmarkBar::State oldState = lastState_;
1489 lastState_ = currentState_;
1490 isAnimationRunning_ = NO;
1492 // Notify our delegate.
1493 [delegate_ bookmarkBar:self
1494 didChangeFromState:oldState
1495 toState:currentState_];
1497 // Update ourselves visually.
1498 [self updateVisibility];
1502 - (void)stopCurrentAnimation {
1503 [[self animatableView] stopAnimation];
1506 // Delegate method for |AnimatableView| (a superclass of
1507 // |BookmarkBarToolbarView|).
1508 - (void)animationDidEnd:(NSAnimation*)animation {
1509 [self finalizeState];
1512 - (void)reconfigureBookmarkBar {
1513 [self setManagedBookmarksButtonVisibility];
1514 [self setSupervisedBookmarksButtonVisibility];
1515 [self redistributeButtonsOnBarAsNeeded];
1516 [self positionRightSideButtons];
1517 [self configureOffTheSideButtonContentsAndVisibility];
1518 [self centerNoItemsLabel];
1521 // Determine if the given |view| can completely fit within the constraint of
1522 // maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum
1523 // width. If the minimum width is not achievable then hide the view. Return YES
1524 // if the view was hidden.
1525 - (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX {
1526 BOOL wasHidden = NO;
1527 // See if the view needs to be narrowed.
1528 NSRect frame = [view frame];
1529 if (NSMaxX(frame) > maxViewX) {
1530 // Resize if more than 30 pixels are showing, otherwise hide.
1531 if (NSMinX(frame) + 30.0 < maxViewX) {
1532 frame.size.width = maxViewX - NSMinX(frame);
1533 [view setFrame:frame];
1535 [view setHidden:YES];
1542 // Bookmark button menu items that open a new window (e.g., open in new window,
1543 // open in incognito, edit, etc.) cause us to lose a mouse-exited event
1544 // on the button, which leaves it in a hover state.
1545 // Since the showsBorderOnlyWhileMouseInside uses a tracking area, simple
1546 // tricks (e.g. sending an extra mouseExited: to the button) don't
1548 // http://crbug.com/129338
1549 - (void)unhighlightBookmark:(const BookmarkNode*)node {
1550 // Only relevant if context menu was opened from a button on the
1552 const BookmarkNode* parent = node->parent();
1553 BookmarkNode::Type parentType = parent->type();
1554 if (parentType == BookmarkNode::BOOKMARK_BAR) {
1555 int index = parent->GetIndexOf(node);
1556 if ((index >= 0) && (static_cast<NSUInteger>(index) < [buttons_ count])) {
1558 [buttons_ objectAtIndex:static_cast<NSUInteger>(index)];
1559 if ([button showsBorderOnlyWhileMouseInside]) {
1560 [button setShowsBorderOnlyWhileMouseInside:NO];
1561 [button setShowsBorderOnlyWhileMouseInside:YES];
1568 // Adjust the horizontal width, x position and the visibility of the "For quick
1569 // access" text field and "Import bookmarks..." button based on the current
1570 // width of the containing |buttonView_| (which is affected by window width).
1571 - (void)adjustNoItemContainerForMaxX:(CGFloat)maxViewX {
1572 if (![[buttonView_ noItemContainer] isHidden]) {
1573 // Reset initial frames for the two items, then adjust as necessary.
1574 NSTextField* noItemTextfield = [buttonView_ noItemTextfield];
1575 NSRect noItemsRect = originalNoItemsRect_;
1576 NSRect importBookmarksRect = originalImportBookmarksRect_;
1577 if (![appsPageShortcutButton_ isHidden]) {
1578 float width = NSWidth([appsPageShortcutButton_ frame]);
1579 noItemsRect.origin.x += width;
1580 importBookmarksRect.origin.x += width;
1582 if (![managedBookmarksButton_ isHidden]) {
1583 float width = NSWidth([managedBookmarksButton_ frame]);
1584 noItemsRect.origin.x += width;
1585 importBookmarksRect.origin.x += width;
1587 if (![supervisedBookmarksButton_ isHidden]) {
1588 float width = NSWidth([supervisedBookmarksButton_ frame]);
1589 noItemsRect.origin.x += width;
1590 importBookmarksRect.origin.x += width;
1592 [noItemTextfield setFrame:noItemsRect];
1593 [noItemTextfield setHidden:NO];
1594 NSButton* importBookmarksButton = [buttonView_ importBookmarksButton];
1595 [importBookmarksButton setFrame:importBookmarksRect];
1596 [importBookmarksButton setHidden:NO];
1597 // Check each to see if they need to be shrunk or hidden.
1598 if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX])
1599 [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX];
1603 // Scans through all buttons from left to right, calculating from scratch where
1604 // they should be based on the preceding widths, until it finds the one
1606 // Returns NSZeroRect if there is no such button in the bookmark bar.
1607 // Enables you to work out where a button will end up when it is done animating.
1608 - (NSRect)finalRectOfButton:(BookmarkButton*)wantedButton {
1609 CGFloat left = bookmarks::kBookmarkLeftMargin;
1610 NSRect buttonFrame = NSZeroRect;
1612 // Draw the apps bookmark if needed.
1613 if (![appsPageShortcutButton_ isHidden]) {
1614 left = NSMaxX([appsPageShortcutButton_ frame]) +
1615 bookmarks::kBookmarkHorizontalPadding;
1618 // Draw the managed bookmarks folder if needed.
1619 if (![managedBookmarksButton_ isHidden]) {
1620 left = NSMaxX([managedBookmarksButton_ frame]) +
1621 bookmarks::kBookmarkHorizontalPadding;
1624 // Draw the supervised bookmarks folder if needed.
1625 if (![supervisedBookmarksButton_ isHidden]) {
1626 left = NSMaxX([supervisedBookmarksButton_ frame]) +
1627 bookmarks::kBookmarkHorizontalPadding;
1630 for (NSButton* button in buttons_.get()) {
1631 // Hidden buttons get no space.
1632 if ([button isHidden])
1634 buttonFrame = [button frame];
1635 buttonFrame.origin.x = left;
1636 left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding;
1637 if (button == wantedButton)
1643 // Calculates the final position of the last button in the bar.
1644 // We can't just use [[self buttons] lastObject] frame] because the button
1645 // may be animating currently.
1646 - (NSRect)finalRectOfLastButton {
1647 return [self finalRectOfButton:[[self buttons] lastObject]];
1650 - (CGFloat)buttonViewMaxXWithOffTheSideButtonIsVisible:(BOOL)visible {
1651 CGFloat maxViewX = NSMaxX([buttonView_ bounds]);
1652 // If necessary, pull in the width to account for the Other Bookmarks button.
1653 if ([self setOtherBookmarksButtonVisibility]) {
1654 maxViewX = [otherBookmarksButton_ frame].origin.x -
1655 bookmarks::kBookmarkRightMargin;
1658 [self positionRightSideButtons];
1659 // If we're already overflowing, then we need to account for the chevron.
1662 [offTheSideButton_ frame].origin.x - bookmarks::kBookmarkRightMargin;
1668 - (void)redistributeButtonsOnBarAsNeeded {
1669 const BookmarkNode* node = bookmarkModel_->bookmark_bar_node();
1670 NSInteger barCount = node->child_count();
1672 // Determine the current maximum extent of the visible buttons.
1673 [self positionRightSideButtons];
1674 BOOL offTheSideButtonVisible = (barCount > displayedButtonCount_);
1675 CGFloat maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:
1676 offTheSideButtonVisible];
1678 // As a result of pasting or dragging, the bar may now have more buttons
1679 // than will fit so remove any which overflow. They will be shown in
1680 // the off-the-side folder.
1681 while (displayedButtonCount_ > 0) {
1682 BookmarkButton* button = [buttons_ lastObject];
1683 if (NSMaxX([self finalRectOfLastButton]) < maxViewX)
1685 [buttons_ removeLastObject];
1686 [button setDelegate:nil];
1687 [button removeFromSuperview];
1688 --displayedButtonCount_;
1689 // Account for the fact that the chevron might now be visible.
1690 if (!offTheSideButtonVisible) {
1691 offTheSideButtonVisible = YES;
1692 maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:YES];
1696 // As a result of cutting, deleting and dragging, the bar may now have room
1697 // for more buttons.
1699 if (displayedButtonCount_ > 0) {
1700 xOffset = NSMaxX([self finalRectOfLastButton]) +
1701 bookmarks::kBookmarkHorizontalPadding;
1702 } else if (![managedBookmarksButton_ isHidden]) {
1703 xOffset = NSMaxX([managedBookmarksButton_ frame]) +
1704 bookmarks::kBookmarkHorizontalPadding;
1705 } else if (![supervisedBookmarksButton_ isHidden]) {
1706 xOffset = NSMaxX([supervisedBookmarksButton_ frame]) +
1707 bookmarks::kBookmarkHorizontalPadding;
1708 } else if (![appsPageShortcutButton_ isHidden]) {
1709 xOffset = NSMaxX([appsPageShortcutButton_ frame]) +
1710 bookmarks::kBookmarkHorizontalPadding;
1712 xOffset = bookmarks::kBookmarkLeftMargin -
1713 bookmarks::kBookmarkHorizontalPadding;
1715 for (int i = displayedButtonCount_; i < barCount; ++i) {
1716 const BookmarkNode* child = node->GetChild(i);
1717 BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
1718 // If we're testing against the last possible button then account
1719 // for the chevron no longer needing to be shown.
1720 if (i == barCount - 1)
1721 maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:NO];
1722 if (NSMaxX([button frame]) > maxViewX) {
1723 [button setDelegate:nil];
1726 ++displayedButtonCount_;
1727 [buttons_ addObject:button];
1728 [buttonView_ addSubview:button];
1731 // While we're here, adjust the horizontal width and the visibility
1732 // of the "For quick access" and "Import bookmarks..." text fields.
1733 if (![buttons_ count])
1734 [self adjustNoItemContainerForMaxX:maxViewX];
1737 #pragma mark Private Methods Exposed for Testing
1739 - (BookmarkBarView*)buttonView {
1743 - (NSMutableArray*)buttons {
1744 return buttons_.get();
1747 - (NSButton*)offTheSideButton {
1748 return offTheSideButton_;
1751 - (NSButton*)appsPageShortcutButton {
1752 return appsPageShortcutButton_;
1755 - (BOOL)offTheSideButtonIsHidden {
1756 return [offTheSideButton_ isHidden];
1759 - (BOOL)appsPageShortcutButtonIsHidden {
1760 return [appsPageShortcutButton_ isHidden];
1763 - (BookmarkButton*)otherBookmarksButton {
1764 return otherBookmarksButton_.get();
1767 - (BookmarkBarFolderController*)folderController {
1768 return folderController_;
1771 - (id)folderTarget {
1772 return folderTarget_.get();
1775 - (int)displayedButtonCount {
1776 return displayedButtonCount_;
1779 // Delete all buttons (bookmarks, chevron, "other bookmarks") from the
1780 // bookmark bar; reset knowledge of bookmarks.
1781 - (void)clearBookmarkBar {
1782 [self stopPulsingBookmarkNode];
1783 for (BookmarkButton* button in buttons_.get()) {
1784 [button setDelegate:nil];
1785 [button removeFromSuperview];
1787 [buttons_ removeAllObjects];
1788 [self clearMenuTagMap];
1789 displayedButtonCount_ = 0;
1791 // Make sure there are no stale pointers in the pasteboard. This
1792 // can be important if a bookmark is deleted (via bookmark sync)
1793 // while in the middle of a drag. The "drag completed" code
1794 // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is
1795 // careful enough to bail if there is no data found at "drop" time.
1796 [[NSPasteboard pasteboardWithName:NSDragPboard] clearContents];
1799 // Return an autoreleased NSCell suitable for a bookmark button.
1800 // TODO(jrg): move much of the cell config into the BookmarkButtonCell class.
1801 - (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node {
1802 NSImage* image = node ? [self faviconForNode:node] : nil;
1803 BookmarkButtonCell* cell =
1804 [BookmarkButtonCell buttonCellForNode:node
1807 menuController:contextMenuController_];
1808 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
1810 // Note: a quirk of setting a cell's text color is that it won't work
1811 // until the cell is associated with a button, so we can't theme the cell yet.
1816 // Return an autoreleased NSCell suitable for a special button displayed on the
1817 // bookmark bar that is not attached to any bookmark node.
1818 // TODO(jrg): move much of the cell config into the BookmarkButtonCell class.
1819 - (BookmarkButtonCell*)cellForCustomButtonWithText:(NSString*)text
1820 image:(NSImage*)image {
1821 BookmarkButtonCell* cell =
1822 [BookmarkButtonCell buttonCellWithText:text
1824 menuController:contextMenuController_];
1825 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
1827 // Note: a quirk of setting a cell's text color is that it won't work
1828 // until the cell is associated with a button, so we can't theme the cell yet.
1833 // Returns a frame appropriate for the given bookmark cell, suitable
1834 // for creating an NSButton that will contain it. |xOffset| is the X
1835 // offset for the frame; it is increased to be an appropriate X offset
1836 // for the next button.
1837 - (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell
1838 xOffset:(int*)xOffset {
1840 NSRect bounds = [buttonView_ bounds];
1841 bounds.size.height = bookmarks::kBookmarkButtonHeight;
1843 NSRect frame = NSInsetRect(bounds,
1844 bookmarks::kBookmarkHorizontalPadding,
1845 bookmarks::kBookmarkVerticalPadding);
1846 frame.size.width = [self widthForBookmarkButtonCell:cell];
1848 // Add an X offset based on what we've already done
1849 frame.origin.x += *xOffset;
1851 // And up the X offset for next time.
1852 *xOffset = NSMaxX(frame);
1857 // A bookmark button's contents changed. Check for growth
1858 // (e.g. increase the width up to the maximum). If we grew, move
1859 // other bookmark buttons over.
1860 - (void)checkForBookmarkButtonGrowth:(NSButton*)changedButton {
1861 NSRect frame = [changedButton frame];
1862 CGFloat desiredSize = [self widthForBookmarkButtonCell:[changedButton cell]];
1863 CGFloat delta = desiredSize - frame.size.width;
1865 frame.size.width = desiredSize;
1866 [changedButton setFrame:frame];
1867 for (NSButton* button in buttons_.get()) {
1868 NSRect buttonFrame = [button frame];
1869 if (buttonFrame.origin.x > frame.origin.x) {
1870 buttonFrame.origin.x += delta;
1871 [button setFrame:buttonFrame];
1875 // We may have just crossed a threshold to enable the off-the-side
1877 [self configureOffTheSideButtonContentsAndVisibility];
1880 // Called when our controlled frame has changed size.
1881 - (void)frameDidChange {
1882 if (!bookmarkModel_->loaded())
1884 [self updateTheme:[[[self view] window] themeProvider]];
1885 [self reconfigureBookmarkBar];
1888 // Given a NSMenuItem tag, return the appropriate bookmark node id.
1889 - (int64)nodeIdFromMenuTag:(int32)tag {
1890 return menuTagMap_[tag];
1893 // Create and return a new tag for the given node id.
1894 - (int32)menuTagFromNodeId:(int64)menuid {
1895 int tag = seedId_++;
1896 menuTagMap_[tag] = menuid;
1900 // Adapt appearance of buttons to the current theme. Called after
1901 // theme changes, or when our view is added to the view hierarchy.
1902 // Oddly, the view pings us instead of us pinging our view. This is
1903 // because our trigger is an [NSView viewWillMoveToWindow:], which the
1904 // controller doesn't normally know about. Otherwise we don't have
1905 // access to the theme before we know what window we will be on.
1906 - (void)updateTheme:(ui::ThemeProvider*)themeProvider {
1910 themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
1911 for (BookmarkButton* button in buttons_.get()) {
1912 BookmarkButtonCell* cell = [button cell];
1913 [cell setTextColor:color];
1915 [[managedBookmarksButton_ cell] setTextColor:color];
1916 [[supervisedBookmarksButton_ cell] setTextColor:color];
1917 [[otherBookmarksButton_ cell] setTextColor:color];
1918 [[appsPageShortcutButton_ cell] setTextColor:color];
1921 // Return YES if the event indicates an exit from the bookmark bar
1922 // folder menus. E.g. "click outside" of the area we are watching.
1923 // At this time we are watching the area that includes all popup
1924 // bookmark folder windows.
1925 - (BOOL)isEventAnExitEvent:(NSEvent*)event {
1926 NSWindow* eventWindow = [event window];
1927 NSWindow* myWindow = [[self view] window];
1928 switch ([event type]) {
1929 case NSLeftMouseDown:
1930 case NSRightMouseDown:
1931 // If the click is in my window but NOT in the bookmark bar, consider
1932 // it a click 'outside'. Clicks directly on an active button (i.e. one
1933 // that is a folder and for which its folder menu is showing) are 'in'.
1934 // All other clicks on the bookmarks bar are counted as 'outside'
1935 // because they should close any open bookmark folder menu.
1936 if (eventWindow == myWindow) {
1938 [[eventWindow contentView] hitTest:[event locationInWindow]];
1939 if (hitView == [folderController_ parentButton])
1941 if (![hitView isDescendantOf:[self view]] || hitView == buttonView_)
1944 // If a click in a bookmark bar folder window and that isn't
1945 // one of my bookmark bar folders, YES is click outside.
1946 if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow
1952 // Event hooks often see the same keydown event twice due to the way key
1953 // events get dispatched and redispatched, so ignore if this keydown
1954 // event has the EXACT same timestamp as the previous keydown.
1955 static NSTimeInterval lastKeyDownEventTime;
1956 NSTimeInterval thisTime = [event timestamp];
1957 if (lastKeyDownEventTime != thisTime) {
1958 lastKeyDownEventTime = thisTime;
1959 if ([event modifierFlags] & NSCommandKeyMask)
1961 else if (folderController_)
1962 return [folderController_ handleInputText:[event characters]];
1968 case NSLeftMouseDragged:
1969 // We can get here with the following sequence:
1970 // - open a bookmark folder
1971 // - right-click (and unclick) on it to open context menu
1972 // - move mouse to window titlebar then click-drag it by the titlebar
1973 // http://crbug.com/49333
1981 #pragma mark Drag & Drop
1983 // Find something like std::is_between<T>? I can't believe one doesn't exist.
1984 static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
1985 return ((value >= low) && (value <= high));
1988 // Return the proposed drop target for a hover open button from the
1989 // given array, or nil if none. We use this for distinguishing
1990 // between a hover-open candidate or drop-indicator draw.
1991 // Helper for buttonForDroppingOnAtPoint:.
1992 // Get UI review on "middle half" ness.
1993 // http://crbug.com/36276
1994 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point
1995 fromArray:(NSArray*)array {
1996 for (BookmarkButton* button in array) {
1997 // Hidden buttons can overlap valid visible buttons, just ignore.
1998 if ([button isHidden])
2000 // Break early if we've gone too far.
2001 if ((NSMinX([button frame]) > point.x) || (![button superview]))
2003 // Careful -- this only applies to the bar with horiz buttons.
2004 // Intentionally NOT using NSPointInRect() so that scrolling into
2005 // a submenu doesn't cause it to be closed.
2006 if (ValueInRangeInclusive(NSMinX([button frame]),
2008 NSMaxX([button frame]))) {
2009 // Over a button but let's be a little more specific (make sure
2010 // it's over the middle half, not just over it).
2011 NSRect frame = [button frame];
2012 NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0);
2013 if (ValueInRangeInclusive(NSMinX(middleHalfOfButton),
2015 NSMaxX(middleHalfOfButton))) {
2016 // It makes no sense to drop on a non-folder; there is no hover.
2017 if (![button isFolder])
2022 // Over a button but not over the middle half.
2027 // Not hovering over a button.
2031 // Return the proposed drop target for a hover open button, or nil if
2032 // none. Works with both the bookmark buttons and the "Other
2033 // Bookmarks" button. Point is in [self view] coordinates.
2034 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
2035 point = [[self view] convertPoint:point
2036 fromView:[[[self view] window] contentView]];
2038 // If there's a hover button, return it if the point is within its bounds.
2039 // Since the logic in -buttonForDroppingOnAtPoint:fromArray: only matches a
2040 // button when the point is over the middle half, this is needed to prevent
2041 // the button's folder being closed if the mouse temporarily leaves the
2042 // middle half but is still within the button bounds.
2043 if (hoverButton_ && NSPointInRect(point, [hoverButton_ frame]))
2044 return hoverButton_.get();
2046 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point
2047 fromArray:buttons_.get()];
2048 // One more chance -- try "Other Bookmarks" and "off the side" (if visible).
2049 // This is different than BookmarkBarFolderController.
2051 NSMutableArray* array = [NSMutableArray array];
2052 if (![self offTheSideButtonIsHidden])
2053 [array addObject:offTheSideButton_];
2054 [array addObject:otherBookmarksButton_];
2055 button = [self buttonForDroppingOnAtPoint:point
2061 - (int)indexForDragToPoint:(NSPoint)point {
2062 // TODO(jrg): revisit position info based on UI team feedback.
2063 // dropLocation is in bar local coordinates.
2064 NSPoint dropLocation =
2065 [[self view] convertPoint:point
2066 fromView:[[[self view] window] contentView]];
2067 BookmarkButton* buttonToTheRightOfDraggedButton = nil;
2068 for (BookmarkButton* button in buttons_.get()) {
2069 CGFloat midpoint = NSMidX([button frame]);
2070 if (dropLocation.x <= midpoint) {
2071 buttonToTheRightOfDraggedButton = button;
2075 if (buttonToTheRightOfDraggedButton) {
2076 const BookmarkNode* afterNode =
2077 [buttonToTheRightOfDraggedButton bookmarkNode];
2079 int index = afterNode->parent()->GetIndexOf(afterNode);
2080 // Make sure we don't get confused by buttons which aren't visible.
2081 return std::min(index, displayedButtonCount_);
2084 // If nothing is to my right I am at the end!
2085 return displayedButtonCount_;
2088 // TODO(mrossetti,jrg): Yet more duplicated code.
2089 // http://crbug.com/35966
2090 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
2094 // Drop destination.
2095 const BookmarkNode* destParent = NULL;
2098 // First check if we're dropping on a button. If we have one, and
2099 // it's a folder, drop in it.
2100 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
2101 if ([button isFolder]) {
2102 destParent = [button bookmarkNode];
2103 // Drop it at the end.
2104 destIndex = [button bookmarkNode]->child_count();
2106 // Else we're dropping somewhere on the bar, so find the right spot.
2107 destParent = bookmarkModel_->bookmark_bar_node();
2108 destIndex = [self indexForDragToPoint:point];
2111 if (!managedBookmarkService_->CanBeEditedByUser(destParent))
2113 if (!managedBookmarkService_->CanBeEditedByUser(sourceNode))
2116 // Be sure we don't try and drop a folder into itself.
2117 if (sourceNode != destParent) {
2119 bookmarkModel_->Copy(sourceNode, destParent, destIndex);
2121 bookmarkModel_->Move(sourceNode, destParent, destIndex);
2124 [self closeFolderAndStopTrackingMenus];
2126 // Movement of a node triggers observers (like us) to rebuild the
2127 // bar so we don't have to do so explicitly.
2132 - (void)draggingEnded:(id<NSDraggingInfo>)info {
2133 [self closeFolderAndStopTrackingMenus];
2134 [[BookmarkButton draggedButton] setHidden:NO];
2135 [self resetAllButtonPositionsWithAnimation:YES];
2138 // Set insertionPos_ and hasInsertionPos_, and make insertion space for a
2139 // hypothetical drop with the new button having a left edge of |where|.
2140 // Gets called only by our view.
2141 - (void)setDropInsertionPos:(CGFloat)where {
2142 if (!hasInsertionPos_ || where != insertionPos_) {
2143 insertionPos_ = where;
2144 hasInsertionPos_ = YES;
2146 if (![supervisedBookmarksButton_ isHidden]) {
2147 left = NSMaxX([supervisedBookmarksButton_ frame]) +
2148 bookmarks::kBookmarkHorizontalPadding;
2149 } else if (![managedBookmarksButton_ isHidden]) {
2150 left = NSMaxX([managedBookmarksButton_ frame]) +
2151 bookmarks::kBookmarkHorizontalPadding;
2152 } else if (![appsPageShortcutButton_ isHidden]) {
2153 left = NSMaxX([appsPageShortcutButton_ frame]) +
2154 bookmarks::kBookmarkHorizontalPadding;
2156 left = bookmarks::kBookmarkLeftMargin;
2158 CGFloat paddingWidth = bookmarks::kDefaultBookmarkWidth;
2159 BookmarkButton* draggedButton = [BookmarkButton draggedButton];
2160 if (draggedButton) {
2161 paddingWidth = std::min(bookmarks::kDefaultBookmarkWidth,
2162 NSWidth([draggedButton frame]));
2164 // Put all the buttons where they belong, with all buttons to the right
2165 // of the insertion point shuffling right to make space for it.
2166 [NSAnimationContext beginGrouping];
2167 [[NSAnimationContext currentContext]
2168 setDuration:kDragAndDropAnimationDuration];
2169 for (NSButton* button in buttons_.get()) {
2170 // Hidden buttons get no space.
2171 if ([button isHidden])
2173 NSRect buttonFrame = [button frame];
2174 buttonFrame.origin.x = left;
2175 // Update "left" for next time around.
2176 left += buttonFrame.size.width;
2177 if (left > insertionPos_)
2178 buttonFrame.origin.x += paddingWidth;
2179 left += bookmarks::kBookmarkHorizontalPadding;
2180 if (innerContentAnimationsEnabled_)
2181 [[button animator] setFrame:buttonFrame];
2183 [button setFrame:buttonFrame];
2185 [NSAnimationContext endGrouping];
2189 // Put all visible bookmark bar buttons in their normal locations, either with
2190 // or without animation according to the |animate| flag.
2191 // This is generally useful, so is called from various places internally.
2192 - (void)resetAllButtonPositionsWithAnimation:(BOOL)animate {
2194 // Position the apps bookmark if needed.
2195 CGFloat left = bookmarks::kBookmarkLeftMargin;
2196 if (![appsPageShortcutButton_ isHidden]) {
2198 bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding;
2200 [self frameForBookmarkButtonFromCell:[appsPageShortcutButton_ cell]
2202 [appsPageShortcutButton_ setFrame:frame];
2203 left = xOffset + bookmarks::kBookmarkHorizontalPadding;
2206 // Position the managed bookmarks folder if needed.
2207 if (![managedBookmarksButton_ isHidden]) {
2210 [self frameForBookmarkButtonFromCell:[managedBookmarksButton_ cell]
2212 [managedBookmarksButton_ setFrame:frame];
2213 left = xOffset + bookmarks::kBookmarkHorizontalPadding;
2216 // Position the supervised bookmarks folder if needed.
2217 if (![supervisedBookmarksButton_ isHidden]) {
2220 [self frameForBookmarkButtonFromCell:[supervisedBookmarksButton_ cell]
2222 [supervisedBookmarksButton_ setFrame:frame];
2223 left = xOffset + bookmarks::kBookmarkHorizontalPadding;
2226 animate &= innerContentAnimationsEnabled_;
2228 for (NSButton* button in buttons_.get()) {
2229 // Hidden buttons get no space.
2230 if ([button isHidden])
2232 NSRect buttonFrame = [button frame];
2233 buttonFrame.origin.x = left;
2234 left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding;
2236 [[button animator] setFrame:buttonFrame];
2238 [button setFrame:buttonFrame];
2242 // Clear insertion flag, remove insertion space and put all visible bookmark
2243 // bar buttons in their normal locations.
2244 // Gets called only by our view.
2245 - (void)clearDropInsertionPos {
2246 if (hasInsertionPos_) {
2247 hasInsertionPos_ = NO;
2248 [self resetAllButtonPositionsWithAnimation:YES];
2252 #pragma mark Bridge Notification Handlers
2254 // TODO(jrg): for now this is brute force.
2255 - (void)loaded:(BookmarkModel*)model {
2256 DCHECK(model == bookmarkModel_);
2257 if (!model->loaded())
2260 // If this is a rebuild request while we have a folder open, close it.
2261 // TODO(mrossetti): Eliminate the need for this because it causes the folder
2262 // menu to disappear after a cut/copy/paste/delete change.
2263 // See: http://crbug.com/36614
2264 if (folderController_)
2265 [self closeAllBookmarkFolders];
2267 // Brute force nuke and build.
2268 savedFrameWidth_ = NSWidth([[self view] frame]);
2269 const BookmarkNode* node = model->bookmark_bar_node();
2270 [self clearBookmarkBar];
2271 [self createAppsPageShortcutButton];
2272 [self createManagedBookmarksButton];
2273 [self createSupervisedBookmarksButton];
2274 [self addNodesToButtonList:node];
2275 [self createOtherBookmarksButton];
2276 [self updateTheme:[[[self view] window] themeProvider]];
2277 [self positionRightSideButtons];
2278 [self addButtonsToView];
2279 [self configureOffTheSideButtonContentsAndVisibility];
2280 [self reconfigureBookmarkBar];
2283 - (void)beingDeleted:(BookmarkModel*)model {
2284 // The browser may be being torn down; little is safe to do. As an
2285 // example, it may not be safe to clear the pasteboard.
2286 // http://crbug.com/38665
2289 - (void)nodeAdded:(BookmarkModel*)model
2290 parent:(const BookmarkNode*)newParent index:(int)newIndex {
2291 // If a context menu is open, close it.
2292 [self cancelMenuTracking];
2294 const BookmarkNode* newNode = newParent->GetChild(newIndex);
2295 id<BookmarkButtonControllerProtocol> newController =
2296 [self controllerForNode:newParent];
2297 [newController addButtonForNode:newNode atIndex:newIndex];
2298 // If we go from 0 --> 1 bookmarks we may need to hide the
2299 // "bookmarks go here" text container.
2300 [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()];
2301 // Cope with chevron or "Other Bookmarks" buttons possibly changing state.
2302 [self reconfigureBookmarkBar];
2305 // TODO(jrg): for now this is brute force.
2306 - (void)nodeChanged:(BookmarkModel*)model
2307 node:(const BookmarkNode*)node {
2308 [self loaded:model];
2311 - (void)nodeMoved:(BookmarkModel*)model
2312 oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex
2313 newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex {
2314 const BookmarkNode* movedNode = newParent->GetChild(newIndex);
2315 id<BookmarkButtonControllerProtocol> oldController =
2316 [self controllerForNode:oldParent];
2317 id<BookmarkButtonControllerProtocol> newController =
2318 [self controllerForNode:newParent];
2319 if (newController == oldController) {
2320 [oldController moveButtonFromIndex:oldIndex toIndex:newIndex];
2322 [oldController removeButton:oldIndex animate:NO];
2323 [newController addButtonForNode:movedNode atIndex:newIndex];
2325 // If the bar is one of the parents we may need to update the visibility
2326 // of the "bookmarks go here" presentation.
2327 [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()];
2328 // Cope with chevron or "Other Bookmarks" buttons possibly changing state.
2329 [self reconfigureBookmarkBar];
2332 - (void)nodeRemoved:(BookmarkModel*)model
2333 parent:(const BookmarkNode*)oldParent index:(int)index {
2334 // If a context menu is open, close it.
2335 [self cancelMenuTracking];
2337 // Locate the parent node. The parent may not be showing, in which case
2339 id<BookmarkButtonControllerProtocol> parentController =
2340 [self controllerForNode:oldParent];
2341 [parentController removeButton:index animate:YES];
2342 // If we go from 1 --> 0 bookmarks we may need to show the
2343 // "bookmarks go here" text container.
2344 [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()];
2345 // If we deleted the only item on the "off the side" menu we no
2346 // longer need to show it.
2347 [self reconfigureBookmarkBar];
2350 // TODO(jrg): linear searching is bad.
2351 // Need a BookmarkNode-->NSCell mapping.
2353 // TODO(jrg): if the bookmark bar is open on launch, we see the
2354 // buttons all placed, then "scooted over" as the favicons load. If
2355 // this looks bad I may need to change widthForBookmarkButtonCell to
2356 // add space for an image even if not there on the assumption that
2357 // favicons will eventually load.
2358 - (void)nodeFaviconLoaded:(BookmarkModel*)model
2359 node:(const BookmarkNode*)node {
2360 for (BookmarkButton* button in buttons_.get()) {
2361 const BookmarkNode* cellnode = [button bookmarkNode];
2362 if (cellnode == node) {
2363 [[button cell] setBookmarkCellText:[button title]
2364 image:[self faviconForNode:node]];
2365 // Adding an image means we might need more room for the
2366 // bookmark. Test for it by growing the button (if needed)
2367 // and shifting everything else over.
2368 [self checkForBookmarkButtonGrowth:button];
2373 if (folderController_)
2374 [folderController_ faviconLoadedForNode:node];
2377 // TODO(jrg): for now this is brute force.
2378 - (void)nodeChildrenReordered:(BookmarkModel*)model
2379 node:(const BookmarkNode*)node {
2380 [self loaded:model];
2383 #pragma mark BookmarkBarState Protocol
2385 // (BookmarkBarState protocol)
2387 return barIsEnabled_ && (currentState_ == BookmarkBar::SHOW ||
2388 currentState_ == BookmarkBar::DETACHED ||
2389 lastState_ == BookmarkBar::SHOW ||
2390 lastState_ == BookmarkBar::DETACHED);
2393 // (BookmarkBarState protocol)
2394 - (BOOL)isInState:(BookmarkBar::State)state {
2395 return currentState_ == state && ![self isAnimationRunning];
2398 // (BookmarkBarState protocol)
2399 - (BOOL)isAnimatingToState:(BookmarkBar::State)state {
2400 return currentState_ == state && [self isAnimationRunning];
2403 // (BookmarkBarState protocol)
2404 - (BOOL)isAnimatingFromState:(BookmarkBar::State)state {
2405 return lastState_ == state && [self isAnimationRunning];
2408 // (BookmarkBarState protocol)
2409 - (BOOL)isAnimatingFromState:(BookmarkBar::State)fromState
2410 toState:(BookmarkBar::State)toState {
2411 return lastState_ == fromState &&
2412 currentState_ == toState &&
2413 [self isAnimationRunning];
2416 // (BookmarkBarState protocol)
2417 - (BOOL)isAnimatingBetweenState:(BookmarkBar::State)fromState
2418 andState:(BookmarkBar::State)toState {
2419 return [self isAnimatingFromState:fromState toState:toState] ||
2420 [self isAnimatingFromState:toState toState:fromState];
2423 // (BookmarkBarState protocol)
2424 - (CGFloat)detachedMorphProgress {
2425 if ([self isInState:BookmarkBar::DETACHED]) {
2428 if ([self isAnimatingToState:BookmarkBar::DETACHED]) {
2429 return static_cast<CGFloat>(
2430 [[self animatableView] currentAnimationProgress]);
2432 if ([self isAnimatingFromState:BookmarkBar::DETACHED]) {
2433 return static_cast<CGFloat>(
2434 1 - [[self animatableView] currentAnimationProgress]);
2439 #pragma mark BookmarkBarToolbarViewController Protocol
2441 - (int)currentTabContentsHeight {
2442 BrowserWindowController* browserController =
2443 [BrowserWindowController browserWindowControllerForView:[self view]];
2444 return NSHeight([[browserController tabContentArea] frame]);
2447 - (ThemeService*)themeService {
2448 return ThemeServiceFactory::GetForProfile(browser_->profile());
2451 #pragma mark BookmarkButtonDelegate Protocol
2453 - (void)fillPasteboard:(NSPasteboard*)pboard
2454 forDragOfButton:(BookmarkButton*)button {
2455 [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
2458 // BookmarkButtonDelegate protocol implementation. When menus are
2459 // "active" (e.g. you clicked to open one), moving the mouse over
2460 // another folder button should close the 1st and open the 2nd (like
2461 // real menus). We detect and act here.
2462 - (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
2463 DCHECK([sender isKindOfClass:[BookmarkButton class]]);
2465 // If folder menus are not being shown, do nothing. This is different from
2466 // BookmarkBarFolderController's implementation because the bar should NOT
2467 // automatically open folder menus when the mouse passes over a folder
2468 // button while the BookmarkBarFolderController DOES automatically open
2469 // a subfolder menu.
2470 if (!showFolderMenus_)
2473 // From here down: same logic as BookmarkBarFolderController.
2474 // TODO(jrg): find a way to share these 4 non-comment lines?
2475 // http://crbug.com/35966
2476 // If already opened, then we exited but re-entered the button, so do nothing.
2477 if ([folderController_ parentButton] == sender)
2479 // Else open a new one if it makes sense to do so.
2480 const BookmarkNode* node = [sender bookmarkNode];
2481 if (node && node->is_folder()) {
2482 // Update |hoverButton_| so that it corresponds to the open folder.
2483 hoverButton_.reset([sender retain]);
2484 [folderTarget_ openBookmarkFolderFromButton:sender];
2486 // We're over a non-folder bookmark so close any old folders.
2487 [folderController_ close];
2488 folderController_ = nil;
2492 // BookmarkButtonDelegate protocol implementation.
2493 - (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
2494 // Don't care; do nothing.
2495 // This is different behavior that the folder menus.
2498 - (NSWindow*)browserWindow {
2499 return [[self view] window];
2502 - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
2503 return [self canEditBookmarks] &&
2504 [self canEditBookmark:[button bookmarkNode]];
2507 - (void)didDragBookmarkToTrash:(BookmarkButton*)button {
2508 if ([self canDragBookmarkButtonToTrash:button]) {
2509 const BookmarkNode* node = [button bookmarkNode];
2511 bookmarkModel_->Remove(node);
2515 - (void)bookmarkDragDidEnd:(BookmarkButton*)button
2516 operation:(NSDragOperation)operation {
2517 [button setHidden:NO];
2518 [self resetAllButtonPositionsWithAnimation:YES];
2522 #pragma mark BookmarkButtonControllerProtocol
2524 // Close all bookmark folders. "Folder" here is the fake menu for
2525 // bookmark folders, not a button context menu.
2526 - (void)closeAllBookmarkFolders {
2527 [self watchForExitEvent:NO];
2528 [folderController_ close];
2529 folderController_ = nil;
2532 - (void)closeBookmarkFolder:(id)sender {
2533 // We're the top level, so close one means close them all.
2534 [self closeAllBookmarkFolders];
2537 - (BookmarkModel*)bookmarkModel {
2538 return bookmarkModel_;
2541 - (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
2542 return [self canEditBookmarks];
2545 // TODO(jrg): much of this logic is duped with
2546 // [BookmarkBarFolderController draggingEntered:] except when noted.
2547 // http://crbug.com/35966
2548 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
2549 NSPoint point = [info draggingLocation];
2550 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
2552 // Don't allow drops that would result in cycles.
2554 NSData* data = [[info draggingPasteboard]
2555 dataForType:kBookmarkButtonDragType];
2556 if (data && [info draggingSource]) {
2557 BookmarkButton* sourceButton = nil;
2558 [data getBytes:&sourceButton length:sizeof(sourceButton)];
2559 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
2560 const BookmarkNode* destNode = [button bookmarkNode];
2561 if (destNode->HasAncestor(sourceNode))
2566 if ([button isFolder]) {
2567 if (hoverButton_ == button) {
2568 return NSDragOperationMove; // already open or timed to open
2571 // Oops, another one triggered or open.
2572 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_
2574 // Unlike BookmarkBarFolderController, we do not delay the close
2575 // of the previous one. Given the lack of diagonal movement,
2576 // there is no need, and it feels awkward to do so. See
2577 // comments about kDragHoverCloseDelay in
2578 // bookmark_bar_folder_controller.mm for more details.
2579 [[hoverButton_ target] closeBookmarkFolder:hoverButton_];
2580 hoverButton_.reset();
2582 hoverButton_.reset([button retain]);
2583 DCHECK([[hoverButton_ target]
2584 respondsToSelector:@selector(openBookmarkFolderFromButton:)]);
2585 [[hoverButton_ target]
2586 performSelector:@selector(openBookmarkFolderFromButton:)
2587 withObject:hoverButton_
2588 afterDelay:bookmarks::kDragHoverOpenDelay
2589 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
2593 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]];
2594 [[hoverButton_ target] closeBookmarkFolder:hoverButton_];
2595 hoverButton_.reset();
2599 // Thrown away but kept to be consistent with the draggingEntered: interface.
2600 return NSDragOperationMove;
2603 - (void)draggingExited:(id<NSDraggingInfo>)info {
2604 // Only close the folder menu if the user dragged up past the BMB. If the user
2605 // dragged to below the BMB, they might be trying to drop a link into the open
2607 // TODO(asvitkine): Need a way to close the menu if the user dragged below but
2608 // not into the menu.
2609 NSRect bounds = [[self view] bounds];
2610 NSPoint origin = [[self view] convertPoint:bounds.origin toView:nil];
2611 if ([info draggingLocation].y > origin.y + bounds.size.height)
2612 [self closeFolderAndStopTrackingMenus];
2614 // NOT the same as a cancel --> we may have moved the mouse into the submenu.
2616 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]];
2617 hoverButton_.reset();
2621 - (BOOL)dragShouldLockBarVisibility {
2622 return ![self isInState:BookmarkBar::DETACHED] &&
2623 ![self isAnimatingToState:BookmarkBar::DETACHED];
2626 // TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController.
2627 // http://crbug.com/35966
2628 - (BOOL)dragButton:(BookmarkButton*)sourceButton
2631 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
2632 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
2633 return [self dragBookmark:sourceNode to:point copy:copy];
2636 - (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
2638 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
2640 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
2641 NSPoint dropPoint = [info draggingLocation];
2642 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
2643 it != nodes.end(); ++it) {
2644 const BookmarkNode* sourceNode = *it;
2645 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
2651 - (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
2652 std::vector<const BookmarkNode*> dragDataNodes;
2653 BookmarkNodeData dragData;
2654 if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) {
2655 std::vector<const BookmarkNode*> nodes(
2656 dragData.GetNodes(bookmarkModel_, browser_->profile()->GetPath()));
2657 dragDataNodes.assign(nodes.begin(), nodes.end());
2659 return dragDataNodes;
2662 // Return YES if we should show the drop indicator, else NO.
2663 - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
2664 return ![self buttonForDroppingOnAtPoint:point];
2667 // Return the x position for a drop indicator.
2668 - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
2670 CGFloat halfHorizontalPadding = 0.5 * bookmarks::kBookmarkHorizontalPadding;
2671 int destIndex = [self indexForDragToPoint:point];
2672 int numButtons = displayedButtonCount_;
2675 if (![supervisedBookmarksButton_ isHidden]) {
2677 NSMaxX([supervisedBookmarksButton_ frame]) + halfHorizontalPadding;
2678 } else if (![managedBookmarksButton_ isHidden]) {
2679 leftmostX = NSMaxX([managedBookmarksButton_ frame]) + halfHorizontalPadding;
2680 } else if (![appsPageShortcutButton_ isHidden]) {
2681 leftmostX = NSMaxX([appsPageShortcutButton_ frame]) + halfHorizontalPadding;
2683 leftmostX = bookmarks::kBookmarkLeftMargin - halfHorizontalPadding;
2686 // If it's a drop strictly between existing buttons ...
2687 if (destIndex == 0) {
2689 } else if (destIndex > 0 && destIndex < numButtons) {
2690 // ... put the indicator right between the buttons.
2691 BookmarkButton* button =
2692 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex-1)];
2694 NSRect buttonFrame = [button frame];
2695 x = NSMaxX(buttonFrame) + halfHorizontalPadding;
2697 // If it's a drop at the end (past the last button, if there are any) ...
2698 } else if (destIndex == numButtons) {
2699 // and if it's past the last button ...
2700 if (numButtons > 0) {
2701 // ... find the last button, and put the indicator to its right.
2702 BookmarkButton* button =
2703 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
2705 x = NSMaxX([button frame]) + halfHorizontalPadding;
2707 // Otherwise, put it right at the beginning.
2718 - (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
2719 // If the bookmarkbar is not in detached mode, lock bar visibility, forcing
2720 // the overlay to stay open when in fullscreen mode.
2721 if (![self isInState:BookmarkBar::DETACHED] &&
2722 ![self isAnimatingToState:BookmarkBar::DETACHED]) {
2723 BrowserWindowController* browserController =
2724 [BrowserWindowController browserWindowControllerForView:[self view]];
2725 [browserController lockBarVisibilityForOwner:child
2731 - (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
2732 // Release bar visibility, allowing the overlay to close if in fullscreen
2734 BrowserWindowController* browserController =
2735 [BrowserWindowController browserWindowControllerForView:[self view]];
2736 [browserController releaseBarVisibilityForOwner:child
2741 // Add a new folder controller as triggered by the given folder button.
2742 - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
2744 // If doing a close/open, make sure the fullscreen chrome doesn't
2745 // have a chance to begin animating away in the middle of things.
2746 BrowserWindowController* browserController =
2747 [BrowserWindowController browserWindowControllerForView:[self view]];
2748 // Confirm we're not re-locking with ourself as an owner before locking.
2749 DCHECK([browserController isBarVisibilityLockedForOwner:self] == NO);
2750 [browserController lockBarVisibilityForOwner:self
2754 if (folderController_)
2755 [self closeAllBookmarkFolders];
2757 // Folder controller, like many window controllers, owns itself.
2759 [[BookmarkBarFolderController alloc]
2760 initWithParentButton:parentButton
2761 parentController:nil
2763 profile:browser_->profile()];
2764 [folderController_ showWindow:self];
2766 // Only BookmarkBarController has this; the
2767 // BookmarkBarFolderController does not.
2768 [self watchForExitEvent:YES];
2770 // No longer need to hold the lock; the folderController_ now owns it.
2771 [browserController releaseBarVisibilityForOwner:self
2776 - (void)openAll:(const BookmarkNode*)node
2777 disposition:(WindowOpenDisposition)disposition {
2778 [self closeFolderAndStopTrackingMenus];
2779 chrome::OpenAll([[self view] window], browser_, node, disposition,
2780 browser_->profile());
2783 - (void)addButtonForNode:(const BookmarkNode*)node
2784 atIndex:(NSInteger)buttonIndex {
2786 bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding;
2787 if (buttonIndex == -1)
2788 buttonIndex = [buttons_ count]; // New button goes at the end.
2789 if (buttonIndex <= (NSInteger)[buttons_ count]) {
2791 BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1];
2792 NSRect targetFrame = [targetButton frame];
2793 newOffset = targetFrame.origin.x + NSWidth(targetFrame) +
2794 bookmarks::kBookmarkHorizontalPadding;
2796 BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset];
2797 ++displayedButtonCount_;
2798 [buttons_ insertObject:newButton atIndex:buttonIndex];
2799 [buttonView_ addSubview:newButton];
2800 [self resetAllButtonPositionsWithAnimation:NO];
2801 // See if any buttons need to be pushed off to or brought in from the side.
2802 [self reconfigureBookmarkBar];
2804 // A button from somewhere else (not the bar) is being moved to the
2805 // off-the-side so insure it gets redrawn if its showing.
2806 [self reconfigureBookmarkBar];
2807 [folderController_ reconfigureMenu];
2811 // TODO(mrossetti): Duplicate code with BookmarkBarFolderController.
2812 // http://crbug.com/35966
2813 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
2814 DCHECK([urls count] == [titles count]);
2815 BOOL nodesWereAdded = NO;
2816 // Figure out where these new bookmarks nodes are to be added.
2817 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
2818 const BookmarkNode* destParent = NULL;
2820 if ([button isFolder]) {
2821 destParent = [button bookmarkNode];
2822 // Drop it at the end.
2823 destIndex = [button bookmarkNode]->child_count();
2825 // Else we're dropping somewhere on the bar, so find the right spot.
2826 destParent = bookmarkModel_->bookmark_bar_node();
2827 destIndex = [self indexForDragToPoint:point];
2830 if (!managedBookmarkService_->CanBeEditedByUser(destParent))
2833 // Don't add the bookmarks if the destination index shows an error.
2834 if (destIndex >= 0) {
2835 // Create and add the new bookmark nodes.
2836 size_t urlCount = [urls count];
2837 for (size_t i = 0; i < urlCount; ++i) {
2839 const char* string = [[urls objectAtIndex:i] UTF8String];
2841 gurl = GURL(string);
2842 // We only expect to receive valid URLs.
2843 DCHECK(gurl.is_valid());
2844 if (gurl.is_valid()) {
2845 bookmarkModel_->AddURL(destParent,
2847 base::SysNSStringToUTF16(
2848 [titles objectAtIndex:i]),
2850 nodesWereAdded = YES;
2854 return nodesWereAdded;
2857 - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
2858 if (fromIndex != toIndex) {
2859 NSInteger buttonCount = (NSInteger)[buttons_ count];
2861 toIndex = buttonCount;
2862 // See if we have a simple move within the bar, which will be the case if
2863 // both button indexes are in the visible space.
2864 if (fromIndex < buttonCount && toIndex < buttonCount) {
2865 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
2866 [buttons_ removeObjectAtIndex:fromIndex];
2867 [buttons_ insertObject:movedButton atIndex:toIndex];
2868 [movedButton setHidden:NO];
2869 [self resetAllButtonPositionsWithAnimation:NO];
2870 } else if (fromIndex < buttonCount) {
2871 // A button is being removed from the bar and added to off-the-side.
2872 // By now the node has already been inserted into the model so the
2873 // button to be added is represented by |toIndex|. Things get
2874 // complicated because the off-the-side is showing and must be redrawn
2875 // while possibly re-laying out the bookmark bar.
2876 [self removeButton:fromIndex animate:NO];
2877 [self reconfigureBookmarkBar];
2878 [folderController_ reconfigureMenu];
2879 } else if (toIndex < buttonCount) {
2880 // A button is being added to the bar and removed from off-the-side.
2881 // By now the node has already been inserted into the model so the
2882 // button to be added is represented by |toIndex|.
2883 const BookmarkNode* node = bookmarkModel_->bookmark_bar_node();
2884 const BookmarkNode* movedNode = node->GetChild(toIndex);
2886 [self addButtonForNode:movedNode atIndex:toIndex];
2887 [self reconfigureBookmarkBar];
2889 // A button is being moved within the off-the-side.
2890 fromIndex -= buttonCount;
2891 toIndex -= buttonCount;
2892 [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex];
2897 - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
2898 if (buttonIndex < (NSInteger)[buttons_ count]) {
2899 // The button being removed is showing in the bar.
2900 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
2901 if (oldButton == [folderController_ parentButton]) {
2902 // If we are deleting a button whose folder is currently open, close it!
2903 [self closeAllBookmarkFolders];
2905 if (animate && innerContentAnimationsEnabled_ && [self isVisible] &&
2906 [[self browserWindow] isMainWindow]) {
2907 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
2908 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
2909 NSZeroSize, nil, nil, nil);
2911 [oldButton setDelegate:nil];
2912 [oldButton removeFromSuperview];
2913 [buttons_ removeObjectAtIndex:buttonIndex];
2914 --displayedButtonCount_;
2915 [self resetAllButtonPositionsWithAnimation:YES];
2916 [self reconfigureBookmarkBar];
2917 } else if (folderController_ &&
2918 [folderController_ parentButton] == offTheSideButton_) {
2919 // The button being removed is in the OTS (off-the-side) and the OTS
2920 // menu is showing so we need to remove the button.
2921 NSInteger index = buttonIndex - displayedButtonCount_;
2922 [folderController_ removeButton:index animate:animate];
2926 - (id<BookmarkButtonControllerProtocol>)controllerForNode:
2927 (const BookmarkNode*)node {
2928 // See if it's in the bar, then if it is in the hierarchy of visible
2930 if (bookmarkModel_->bookmark_bar_node() == node)
2932 return [folderController_ controllerForNode:node];