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