blob: f86c61b8a159d7ebd00275cf66ab750c468be67c [file] [log] [blame]
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001// 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.
4
5#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
6
7#include "base/mac/bundle_locations.h"
8#include "base/mac/mac_util.h"
9#include "base/metrics/histogram.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000010#include "base/prefs/pref_service.h"
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +010011#include "base/strings/sys_string_conversions.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000012#include "chrome/browser/bookmarks/bookmark_model.h"
13#include "chrome/browser/bookmarks/bookmark_model_factory.h"
14#include "chrome/browser/bookmarks/bookmark_utils.h"
15#include "chrome/browser/extensions/extension_service.h"
16#include "chrome/browser/prefs/incognito_mode_prefs.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000017#include "chrome/browser/profiles/profile.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000018#include "chrome/browser/themes/theme_properties.h"
19#include "chrome/browser/themes/theme_service.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000020#import "chrome/browser/themes/theme_service_factory.h"
Ben Murdocheb525c52013-07-10 11:40:50 +010021#include "chrome/browser/ui/bookmarks/bookmark_editor.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000022#include "chrome/browser/ui/bookmarks/bookmark_utils.h"
23#include "chrome/browser/ui/browser.h"
24#include "chrome/browser/ui/browser_list.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000025#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"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000034#import "chrome/browser/ui/cocoa/bookmarks/bookmark_context_menu_cocoa_controller.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000035#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
36#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000037#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"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000040#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"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000046#include "chrome/browser/ui/tabs/tab_strip_model.h"
Ben Murdoch558790d2013-07-30 15:19:42 +010047#include "chrome/browser/ui/webui/ntp/core_app_launcher_handler.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000048#include "chrome/common/extensions/extension_constants.h"
49#include "chrome/common/pref_names.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000050#include "chrome/common/url_constants.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000051#include "content/public/browser/user_metrics.h"
52#include "content/public/browser/web_contents.h"
53#include "content/public/browser/web_contents_view.h"
54#include "grit/generated_resources.h"
55#include "grit/theme_resources.h"
56#include "grit/ui_resources.h"
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010057#import "ui/base/cocoa/cocoa_event_utils.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000058#include "ui/base/l10n/l10n_util_mac.h"
59#include "ui/base/resource/resource_bundle.h"
60#include "ui/gfx/image/image.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000061
62using content::OpenURLParams;
63using content::Referrer;
64using content::UserMetricsAction;
65using content::WebContents;
66
67// Bookmark bar state changing and animations
68//
69// The bookmark bar has three real states: "showing" (a normal bar attached to
70// the toolbar), "hidden", and "detached" (pretending to be part of the web
71// content on the NTP). It can, or at least should be able to, animate between
72// these states. There are several complications even without animation:
73// - The placement of the bookmark bar is done by the BWC, and it needs to know
74// the state in order to place the bookmark bar correctly (immediately below
75// the toolbar when showing, below the infobar when detached).
76// - The "divider" (a black line) needs to be drawn by either the toolbar (when
77// the bookmark bar is hidden or detached) or by the bookmark bar (when it is
78// showing). It should not be drawn by both.
79// - The toolbar needs to vertically "compress" when the bookmark bar is
80// showing. This ensures the proper display of both the bookmark bar and the
81// toolbar, and gives a padded area around the bookmark bar items for right
82// clicks, etc.
83//
84// Our model is that the BWC controls us and also the toolbar. We try not to
85// talk to the browser nor the toolbar directly, instead centralizing control in
86// the BWC. The key method by which the BWC controls us is
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000087// |-updateState:ChangeType:|. This invokes state changes, and at appropriate
88// times we request that the BWC do things for us via either the resize delegate
89// or our general delegate. If the BWC needs any information about what it
90// should do, or tell the toolbar to do, it can then query us back (e.g.,
91// |-isShownAs...|, |-getDesiredToolbarHeightCompression|,
Torne (Richard Coles)58218062012-11-14 11:43:16 +000092// |-toolbarDividerOpacity|, etc.).
93//
94// Animation-related complications:
95// - Compression of the toolbar is touchy during animation. It must not be
96// compressed while the bookmark bar is animating to/from showing (from/to
97// hidden), otherwise it would look like the bookmark bar's contents are
98// sliding out of the controls inside the toolbar. As such, we have to make
99// sure that the bookmark bar is shown at the right location and at the
100// right height (at various points in time).
101// - Showing the divider is also complicated during animation between hidden
102// and showing. We have to make sure that the toolbar does not show the
103// divider despite the fact that it's not compressed. The exception to this
104// is at the beginning/end of the animation when the toolbar is still
105// uncompressed but the bookmark bar has height 0. If we're not careful, we
106// get a flicker at this point.
107// - We have to ensure that we do the right thing if we're told to change state
108// while we're running an animation. The generic/easy thing to do is to jump
109// to the end state of our current animation, and (if the new state change
110// again involves an animation) begin the new animation. We can do better
111// than that, however, and sometimes just change the current animation to go
112// to the new end state (e.g., by "reversing" the animation in the showing ->
113// hidden -> showing case). We also have to ensure that demands to
114// immediately change state are always honoured.
115//
116// Pointers to animation logic:
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000117// - |-moveToState:withAnimation:| starts animations, deciding which ones we
118// know how to handle.
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000119// - |-doBookmarkBarAnimation| has most of the actual logic.
120// - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain
121// related logic.
122// - The BWC's |-layoutSubviews| needs to know how to position things.
123// - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and
124// |-bookmarkBar:willAnimateFromState:toState:| in order to inform the
125// toolbar of required changes.
126
127namespace {
128
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000129// Duration of the bookmark bar animations.
130const NSTimeInterval kBookmarkBarAnimationDuration = 0.12;
131
132void RecordAppLaunch(Profile* profile, GURL url) {
133 DCHECK(profile->GetExtensionService());
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000134 const extensions::Extension* extension =
135 profile->GetExtensionService()->GetInstalledApp(url);
136 if (!extension)
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000137 return;
138
Ben Murdoch558790d2013-07-30 15:19:42 +0100139 CoreAppLauncherHandler::RecordAppLaunchType(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000140 extension_misc::APP_LAUNCH_BOOKMARK_BAR,
141 extension->GetType());
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000142}
143
144} // namespace
145
146@interface BookmarkBarController(Private)
147
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000148// Moves to the given next state (from the current state), possibly animating.
149// If |animate| is NO, it will stop any running animation and jump to the given
150// state. If YES, it may either (depending on implementation) jump to the end of
151// the current animation and begin the next one, or stop the current animation
152// mid-flight and animate to the next state.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000153- (void)moveToState:(BookmarkBar::State)nextState
154 withAnimation:(BOOL)animate;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000155
156// Return the backdrop to the bookmark bar as various types.
157- (BackgroundGradientView*)backgroundGradientView;
158- (AnimatableView*)animatableView;
159
160// Create buttons for all items in the given bookmark node tree.
161// Modifies self->buttons_. Do not add more buttons than will fit on the view.
162- (void)addNodesToButtonList:(const BookmarkNode*)node;
163
164// Create an autoreleased button appropriate for insertion into the bookmark
165// bar. Update |xOffset| with the offset appropriate for the subsequent button.
166- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
167 xOffset:(int*)xOffset;
168
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000169// Puts stuff into the final state without animating, stopping a running
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000170// animation if necessary.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000171- (void)finalizeState;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000172
173// Stops any current animation in its tracks (midway).
174- (void)stopCurrentAnimation;
175
176// Show/hide the bookmark bar.
177// if |animate| is YES, the changes are made using the animator; otherwise they
178// are made immediately.
179- (void)showBookmarkBarWithAnimation:(BOOL)animate;
180
181// Handles animating the resize of the content view. Returns YES if it handled
182// the animation, NO if not (and hence it should be done instantly).
183- (BOOL)doBookmarkBarAnimation;
184
185// |point| is in the base coordinate system of the destination window;
186// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
187// made and inserted into the new location while leaving the bookmark in
188// the old location, otherwise move the bookmark by removing from its old
189// location and inserting into the new location.
190- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
191 to:(NSPoint)point
192 copy:(BOOL)copy;
193
194// Returns the index in the model for a drag to the location given by
195// |point|. This is determined by finding the first button before the center
196// of which |point| falls, scanning left to right. Note that, currently, only
197// the x-coordinate of |point| is considered. Though not currently implemented,
198// we may check for errors, in which case this would return negative value;
199// callers should check for this.
200- (int)indexForDragToPoint:(NSPoint)point;
201
202// Add or remove buttons to/from the bar until it is filled but not overflowed.
203- (void)redistributeButtonsOnBarAsNeeded;
204
205// Determine the nature of the bookmark bar contents based on the number of
206// buttons showing. If too many then show the off-the-side list, if none
207// then show the no items label.
208- (void)reconfigureBookmarkBar;
209
210- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu;
211- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu;
212- (void)tagEmptyMenu:(NSMenu*)menu;
213- (void)clearMenuTagMap;
214- (int)preferredHeight;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000215- (void)addButtonsToView;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000216- (BOOL)setOtherBookmarksButtonVisibility;
217- (BOOL)setAppsPageShortcutButtonVisibility;
218- (BookmarkButton*)customBookmarkButtonForCell:(NSCell*)cell;
219- (void)createOtherBookmarksButton;
220- (void)createAppsPageShortcutButton;
221- (void)openAppsPage:(id)sender;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000222- (void)centerNoItemsLabel;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000223- (void)positionRightSideButtons;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000224- (void)watchForExitEvent:(BOOL)watch;
225- (void)resetAllButtonPositionsWithAnimation:(BOOL)animate;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000226
227@end
228
229@implementation BookmarkBarController
230
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000231@synthesize currentState = currentState_;
232@synthesize lastState = lastState_;
233@synthesize isAnimationRunning = isAnimationRunning_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000234@synthesize delegate = delegate_;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000235@synthesize stateAnimationsEnabled = stateAnimationsEnabled_;
236@synthesize innerContentAnimationsEnabled = innerContentAnimationsEnabled_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000237
238- (id)initWithBrowser:(Browser*)browser
239 initialWidth:(CGFloat)initialWidth
240 delegate:(id<BookmarkBarControllerDelegate>)delegate
241 resizeDelegate:(id<ViewResizer>)resizeDelegate {
242 if ((self = [super initWithNibName:@"BookmarkBar"
243 bundle:base::mac::FrameworkBundle()])) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000244 currentState_ = BookmarkBar::HIDDEN;
245 lastState_ = BookmarkBar::HIDDEN;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000246
247 browser_ = browser;
248 initialWidth_ = initialWidth;
249 bookmarkModel_ = BookmarkModelFactory::GetForProfile(browser_->profile());
250 buttons_.reset([[NSMutableArray alloc] init]);
251 delegate_ = delegate;
252 resizeDelegate_ = resizeDelegate;
253 folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
254
255 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
256 folderImage_.reset(
257 rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER).CopyNSImage());
258 defaultImage_.reset(
259 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
260
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000261 innerContentAnimationsEnabled_ = YES;
262 stateAnimationsEnabled_ = YES;
263
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000264 // Register for theme changes, bookmark button pulsing, ...
265 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
266 [defaultCenter addObserver:self
267 selector:@selector(themeDidChangeNotification:)
268 name:kBrowserThemeDidChangeNotification
269 object:nil];
270 [defaultCenter addObserver:self
271 selector:@selector(pulseBookmarkNotification:)
272 name:bookmark_button::kPulseBookmarkButtonNotification
273 object:nil];
274
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000275 contextMenuController_.reset(
276 [[BookmarkContextMenuCocoaController alloc]
277 initWithBookmarkBarController:self]);
278
279 // This call triggers an -awakeFromNib, which builds the bar, which might
280 // use |folderImage_| and |contextMenuController_|. Ensure it happens after
281 // |folderImage_| is loaded and |contextMenuController_| is created.
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000282 [[self animatableView] setResizeDelegate:resizeDelegate];
283 }
284 return self;
285}
286
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000287- (Browser*)browser {
288 return browser_;
289}
290
291- (BookmarkContextMenuCocoaController*)menuController {
292 return contextMenuController_.get();
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000293}
294
295- (void)pulseBookmarkNotification:(NSNotification*)notification {
296 NSDictionary* dict = [notification userInfo];
297 const BookmarkNode* node = NULL;
298 NSValue *value = [dict objectForKey:bookmark_button::kBookmarkKey];
299 DCHECK(value);
300 if (value)
301 node = static_cast<const BookmarkNode*>([value pointerValue]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000302 NSNumber* number = [dict objectForKey:bookmark_button::kBookmarkPulseFlagKey];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000303 DCHECK(number);
304 BOOL doPulse = number ? [number boolValue] : NO;
305
306 // 3 cases:
307 // button on the bar: flash it
308 // button in "other bookmarks" folder: flash other bookmarks
309 // button in "off the side" folder: flash the chevron
310 for (BookmarkButton* button in [self buttons]) {
311 if ([button bookmarkNode] == node) {
312 [button setIsContinuousPulsing:doPulse];
313 return;
314 }
315 }
316 if ([otherBookmarksButton_ bookmarkNode] == node) {
317 [otherBookmarksButton_ setIsContinuousPulsing:doPulse];
318 return;
319 }
320 if (node->parent() == bookmarkModel_->bookmark_bar_node()) {
321 [offTheSideButton_ setIsContinuousPulsing:doPulse];
322 return;
323 }
324
325 NOTREACHED() << "no bookmark button found to pulse!";
326}
327
328- (void)dealloc {
329 // Clear delegate so it doesn't get called during stopAnimation.
330 [[self animatableView] setResizeDelegate:nil];
331
332 // We better stop any in-flight animation if we're being killed.
333 [[self animatableView] stopAnimation];
334
335 // Remove our view from its superview so it doesn't attempt to reference
336 // it when the controller is gone.
337 //TODO(dmaclach): Remove -- http://crbug.com/25845
338 [[self view] removeFromSuperview];
339
340 // Be sure there is no dangling pointer.
341 if ([[self view] respondsToSelector:@selector(setController:)])
342 [[self view] performSelector:@selector(setController:) withObject:nil];
343
344 // For safety, make sure the buttons can no longer call us.
345 for (BookmarkButton* button in buttons_.get()) {
346 [button setDelegate:nil];
347 [button setTarget:nil];
348 [button setAction:nil];
349 }
350
351 bridge_.reset(NULL);
352 [[NSNotificationCenter defaultCenter] removeObserver:self];
353 [self watchForExitEvent:NO];
354 [super dealloc];
355}
356
357- (void)awakeFromNib {
358 // We default to NOT open, which means height=0.
359 DCHECK([[self view] isHidden]); // Hidden so it's OK to change.
360
361 // Set our initial height to zero, since that is what the superview
362 // expects. We will resize ourselves open later if needed.
363 [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)];
364
365 // Complete init of the "off the side" button, as much as we can.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000366 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
367 [offTheSideButton_ setImage:
368 rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_CHEVRONS).ToNSImage()];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000369 [offTheSideButton_.draggableButton setDraggable:NO];
370 [offTheSideButton_.draggableButton setActsOnMouseDown:YES];
371
372 // We are enabled by default.
373 barIsEnabled_ = YES;
374
375 // Remember the original sizes of the 'no items' and 'import bookmarks'
376 // fields to aid in resizing when the window frame changes.
377 originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame];
378 originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame];
379
380 // To make life happier when the bookmark bar is floating, the chevron is a
381 // child of the button view.
382 [offTheSideButton_ removeFromSuperview];
383 [buttonView_ addSubview:offTheSideButton_];
384
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000385 // When resized we may need to add new buttons, or remove them (if
386 // no longer visible), or add/remove the "off the side" menu.
387 [[self view] setPostsFrameChangedNotifications:YES];
388 [[NSNotificationCenter defaultCenter]
389 addObserver:self
390 selector:@selector(frameDidChange)
391 name:NSViewFrameDidChangeNotification
392 object:[self view]];
393
394 // Watch for things going to or from fullscreen.
395 [[NSNotificationCenter defaultCenter]
396 addObserver:self
397 selector:@selector(willEnterOrLeaveFullscreen:)
398 name:kWillEnterFullscreenNotification
399 object:nil];
400 [[NSNotificationCenter defaultCenter]
401 addObserver:self
402 selector:@selector(willEnterOrLeaveFullscreen:)
403 name:kWillLeaveFullscreenNotification
404 object:nil];
405
406 // Don't pass ourself along (as 'self') until our init is completely
407 // done. Thus, this call is (almost) last.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000408 bridge_.reset(new BookmarkBarBridge(browser_->profile(), self,
409 bookmarkModel_));
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000410}
411
412// Called by our main view (a BookmarkBarView) when it gets moved to a
413// window. We perform operations which need to know the relevant
414// window (e.g. watch for a window close) so they can't be performed
415// earlier (such as in awakeFromNib).
416- (void)viewDidMoveToWindow {
417 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
418
419 // Remove any existing notifications before registering for new ones.
420 [defaultCenter removeObserver:self
421 name:NSWindowWillCloseNotification
422 object:nil];
423 [defaultCenter removeObserver:self
424 name:NSWindowDidResignMainNotification
425 object:nil];
426
427 [defaultCenter addObserver:self
428 selector:@selector(parentWindowWillClose:)
429 name:NSWindowWillCloseNotification
430 object:[[self view] window]];
431 [defaultCenter addObserver:self
432 selector:@selector(parentWindowDidResignMain:)
433 name:NSWindowDidResignMainNotification
434 object:[[self view] window]];
435}
436
437// When going fullscreen we can run into trouble. Our view is removed
438// from the non-fullscreen window before the non-fullscreen window
439// loses key, so our parentDidResignKey: callback never gets called.
440// In addition, a bookmark folder controller needs to be autoreleased
441// (in case it's in the event chain when closed), but the release
442// implicitly needs to happen while it's connected to the original
443// (non-fullscreen) window to "unlock bar visibility". Such a
444// contract isn't honored when going fullscreen with the menu option
445// (not with the keyboard shortcut). We fake it as best we can here.
446// We have a similar problem leaving fullscreen.
447- (void)willEnterOrLeaveFullscreen:(NSNotification*)notification {
448 if (folderController_) {
449 [self childFolderWillClose:folderController_];
450 [self closeFolderAndStopTrackingMenus];
451 }
452}
453
454// NSNotificationCenter callback.
455- (void)parentWindowWillClose:(NSNotification*)notification {
456 [self closeFolderAndStopTrackingMenus];
457}
458
459// NSNotificationCenter callback.
460- (void)parentWindowDidResignMain:(NSNotification*)notification {
461 [self closeFolderAndStopTrackingMenus];
462}
463
464// Change the layout of the bookmark bar's subviews in response to a visibility
465// change (e.g., show or hide the bar) or style change (attached or floating).
466- (void)layoutSubviews {
467 NSRect frame = [[self view] frame];
468 NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame));
469
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000470 // Add padding to the detached bookmark bar.
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000471 // The state of our morph (if any); 1 is total bubble, 0 is the regular bar.
472 CGFloat morph = [self detachedMorphProgress];
Ben Murdocheb525c52013-07-10 11:40:50 +0100473 CGFloat padding = bookmarks::kNTPBookmarkBarPadding;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000474 buttonViewFrame =
475 NSInsetRect(buttonViewFrame, morph * padding, morph * padding);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000476
477 [buttonView_ setFrame:buttonViewFrame];
Ben Murdocheb525c52013-07-10 11:40:50 +0100478
479 // Update bookmark button backgrounds.
480 if ([self isAnimationRunning]) {
481 for (NSButton* button in buttons_.get())
482 [button setNeedsDisplay:YES];
483 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000484}
485
486// We don't change a preference; we only change visibility. Preference changing
487// (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We
488// simply update based on what we're told.
489- (void)updateVisibility {
490 [self showBookmarkBarWithAnimation:NO];
491}
492
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000493- (void)updateAppsPageShortcutButtonVisibility {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100494 if (!appsPageShortcutButton_.get())
495 return;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000496 [self setAppsPageShortcutButtonVisibility];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100497 [self resetAllButtonPositionsWithAnimation:NO];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000498 [self reconfigureBookmarkBar];
499}
500
501- (void)updateHiddenState {
502 BOOL oldHidden = [[self view] isHidden];
503 BOOL newHidden = ![self isVisible];
504 if (oldHidden != newHidden)
505 [[self view] setHidden:newHidden];
506}
507
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000508- (void)setBookmarkBarEnabled:(BOOL)enabled {
509 if (enabled != barIsEnabled_) {
510 barIsEnabled_ = enabled;
511 [self updateVisibility];
512 }
513}
514
515- (CGFloat)getDesiredToolbarHeightCompression {
516 // Some special cases....
517 if (!barIsEnabled_)
518 return 0;
519
520 if ([self isAnimationRunning]) {
521 // No toolbar compression when animating between hidden and showing, nor
522 // between showing and detached.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000523 if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN
524 andState:BookmarkBar::SHOW] ||
525 [self isAnimatingBetweenState:BookmarkBar::SHOW
526 andState:BookmarkBar::DETACHED])
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000527 return 0;
528
529 // If we ever need any other animation cases, code would go here.
530 }
531
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100532 return [self isInState:BookmarkBar::SHOW] ? bookmarks::kBookmarkBarOverlap
533 : 0;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000534}
535
536- (CGFloat)toolbarDividerOpacity {
537 // Some special cases....
538 if ([self isAnimationRunning]) {
539 // In general, the toolbar shouldn't show a divider while we're animating
540 // between showing and hidden. The exception is when our height is < 1, in
541 // which case we can't draw it. It's all-or-nothing (no partial opacity).
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000542 if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN
543 andState:BookmarkBar::SHOW])
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000544 return (NSHeight([[self view] frame]) < 1) ? 1 : 0;
545
546 // The toolbar should show the divider when animating between showing and
547 // detached (but opacity will vary).
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000548 if ([self isAnimatingBetweenState:BookmarkBar::SHOW
549 andState:BookmarkBar::DETACHED])
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000550 return static_cast<CGFloat>([self detachedMorphProgress]);
551
552 // If we ever need any other animation cases, code would go here.
553 }
554
555 // In general, only show the divider when it's in the normal showing state.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000556 return [self isInState:BookmarkBar::SHOW] ? 0 : 1;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000557}
558
559- (NSImage*)faviconForNode:(const BookmarkNode*)node {
560 if (!node)
561 return defaultImage_;
562
563 if (node->is_folder())
564 return folderImage_;
565
566 const gfx::Image& favicon = bookmarkModel_->GetFavicon(node);
567 if (!favicon.IsEmpty())
568 return favicon.ToNSImage();
569
570 return defaultImage_;
571}
572
573- (void)closeFolderAndStopTrackingMenus {
574 showFolderMenus_ = NO;
575 [self closeAllBookmarkFolders];
576}
577
578- (BOOL)canEditBookmarks {
579 PrefService* prefs = browser_->profile()->GetPrefs();
580 return prefs->GetBoolean(prefs::kEditBookmarksEnabled);
581}
582
583- (BOOL)canEditBookmark:(const BookmarkNode*)node {
584 // Don't allow edit/delete of the permanent nodes.
585 if (node == nil || bookmarkModel_->is_permanent_node(node))
586 return NO;
587 return YES;
588}
589
590#pragma mark Actions
591
592// Helper methods called on the main thread by runMenuFlashThread.
593
594- (void)setButtonFlashStateOn:(id)sender {
595 [sender highlight:YES];
596}
597
598- (void)setButtonFlashStateOff:(id)sender {
599 [sender highlight:NO];
600}
601
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000602- (void)cleanupAfterMenuFlashThread:(id)sender {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000603 [self closeFolderAndStopTrackingMenus];
604
605 // Items retained by doMenuFlashOnSeparateThread below.
606 [sender release];
607 [self release];
608}
609
610// End runMenuFlashThread helper methods.
611
612// This call is invoked only by doMenuFlashOnSeparateThread below.
613// It makes the selected BookmarkButton (which is masquerading as a menu item)
614// flash a few times to give confirmation feedback, then it closes the menu.
615// It spends all its time sleeping or scheduling UI work on the main thread.
616- (void)runMenuFlashThread:(id)sender {
617
618 // Check this is not running on the main thread, as it sleeps.
619 DCHECK(![NSThread isMainThread]);
620
621 // Duration of flash phases and number of flashes designed to evoke a
622 // slightly retro "more mac-like than the Mac" feel.
623 // Current Cocoa UI has a barely perceptible flash,probably because Apple
624 // doesn't fire the action til after the animation and so there's a hurry.
625 // As this code is fully asynchronous, it can take its time.
626 const float kBBOnFlashTime = 0.08;
627 const float kBBOffFlashTime = 0.08;
628 const int kBookmarkButtonMenuFlashes = 3;
629
630 for (int count = 0 ; count < kBookmarkButtonMenuFlashes ; count++) {
631 [self performSelectorOnMainThread:@selector(setButtonFlashStateOn:)
632 withObject:sender
633 waitUntilDone:NO];
634 [NSThread sleepForTimeInterval:kBBOnFlashTime];
635 [self performSelectorOnMainThread:@selector(setButtonFlashStateOff:)
636 withObject:sender
637 waitUntilDone:NO];
638 [NSThread sleepForTimeInterval:kBBOffFlashTime];
639 }
640 [self performSelectorOnMainThread:@selector(cleanupAfterMenuFlashThread:)
641 withObject:sender
642 waitUntilDone:NO];
643}
644
645// Non-blocking call which starts the process to make the selected menu item
646// flash a few times to give confirmation feedback, after which it closes the
647// menu. The item is of course actually a BookmarkButton masquerading as a menu
648// item).
649- (void)doMenuFlashOnSeparateThread:(id)sender {
650
651 // Ensure that self and sender don't go away before the animation completes.
652 // These retains are balanced in cleanupAfterMenuFlashThread above.
653 [self retain];
654 [sender retain];
655 [NSThread detachNewThreadSelector:@selector(runMenuFlashThread:)
656 toTarget:self
657 withObject:sender];
658}
659
660- (IBAction)openBookmark:(id)sender {
661 BOOL isMenuItem = [[sender cell] isFolderButtonCell];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000662 BOOL animate = isMenuItem && innerContentAnimationsEnabled_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000663 if (animate)
664 [self doMenuFlashOnSeparateThread:sender];
665 DCHECK([sender respondsToSelector:@selector(bookmarkNode)]);
666 const BookmarkNode* node = [sender bookmarkNode];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000667 DCHECK(node);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000668 WindowOpenDisposition disposition =
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100669 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000670 RecordAppLaunch(browser_->profile(), node->url());
671 [self openURL:node->url() disposition:disposition];
672
673 if (!animate)
674 [self closeFolderAndStopTrackingMenus];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000675 bookmark_utils::RecordBookmarkLaunch([self bookmarkLaunchLocation]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000676}
677
678// Common function to open a bookmark folder of any type.
679- (void)openBookmarkFolder:(id)sender {
680 DCHECK([sender isKindOfClass:[BookmarkButton class]]);
681 DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]);
682
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000683 // Only record the action if it's the initial folder being opened.
684 if (!showFolderMenus_)
685 bookmark_utils::RecordBookmarkFolderOpen([self bookmarkLaunchLocation]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000686 showFolderMenus_ = !showFolderMenus_;
687
688 if (sender == offTheSideButton_)
689 [[sender cell] setStartingChildIndex:displayedButtonCount_];
690
691 // Toggle presentation of bar folder menus.
692 [folderTarget_ openBookmarkFolderFromButton:sender];
693}
694
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000695// Click on a bookmark folder button.
696- (IBAction)openBookmarkFolderFromButton:(id)sender {
697 [self openBookmarkFolder:sender];
698}
699
700// Click on the "off the side" button (chevron), which opens like a folder
701// button but isn't exactly a parent folder.
702- (IBAction)openOffTheSideFolderFromButton:(id)sender {
703 [self openBookmarkFolder:sender];
704}
705
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000706- (IBAction)importBookmarks:(id)sender {
707 chrome::ShowImportDialog(browser_);
708}
709
710#pragma mark Private Methods
711
712// Called after a theme change took place, possibly for a different profile.
713- (void)themeDidChangeNotification:(NSNotification*)notification {
714 [self updateTheme:[[[self view] window] themeProvider]];
715}
716
717// (Private) Method is the same as [self view], but is provided to be explicit.
718- (BackgroundGradientView*)backgroundGradientView {
719 DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]);
720 return (BackgroundGradientView*)[self view];
721}
722
723// (Private) Method is the same as [self view], but is provided to be explicit.
724- (AnimatableView*)animatableView {
725 DCHECK([[self view] isKindOfClass:[AnimatableView class]]);
726 return (AnimatableView*)[self view];
727}
728
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000729- (bookmark_utils::BookmarkLaunchLocation)bookmarkLaunchLocation {
730 return currentState_ == BookmarkBar::DETACHED ?
731 bookmark_utils::LAUNCH_DETACHED_BAR :
732 bookmark_utils::LAUNCH_ATTACHED_BAR;
733}
734
735// Position the right-side buttons including the off-the-side chevron.
736- (void)positionRightSideButtons {
737 int maxX = NSMaxX([[self buttonView] bounds]) -
738 bookmarks::kBookmarkHorizontalPadding;
739 int right = maxX;
740
741 int ignored = 0;
742 NSRect frame = [self frameForBookmarkButtonFromCell:
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000743 [otherBookmarksButton_ cell] xOffset:&ignored];
744 if (![otherBookmarksButton_ isHidden]) {
745 right -= NSWidth(frame);
746 frame.origin.x = right;
747 } else {
748 frame.origin.x = maxX - NSWidth(frame);
749 }
750 [otherBookmarksButton_ setFrame:frame];
751
752 frame = [offTheSideButton_ frame];
753 frame.size.height = bookmarks::kBookmarkFolderButtonHeight;
754 right -= frame.size.width;
755 frame.origin.x = right;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000756 [offTheSideButton_ setFrame:frame];
757}
758
759// Configure the off-the-side button (e.g. specify the node range,
760// check if we should enable or disable it, etc).
761- (void)configureOffTheSideButtonContentsAndVisibility {
762 // If deleting a button while off-the-side is open, buttons may be
763 // promoted from off-the-side to the bar. Accomodate.
764 if (folderController_ &&
765 ([folderController_ parentButton] == offTheSideButton_)) {
766 [folderController_ reconfigureMenu];
767 }
768
769 [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_];
770 [[offTheSideButton_ cell]
771 setBookmarkNode:bookmarkModel_->bookmark_bar_node()];
772 int bookmarkChildren = bookmarkModel_->bookmark_bar_node()->child_count();
773 if (bookmarkChildren > displayedButtonCount_) {
774 [offTheSideButton_ setHidden:NO];
775 } else {
776 // If we just deleted the last item in an off-the-side menu so the
777 // button will be going away, make sure the menu goes away.
778 if (folderController_ &&
779 ([folderController_ parentButton] == offTheSideButton_))
780 [self closeAllBookmarkFolders];
781 // (And hide the button, too.)
782 [offTheSideButton_ setHidden:YES];
783 }
784}
785
786// Main menubar observation code, so we can know to close our fake menus if the
787// user clicks on the actual menubar, as multiple unconnected menus sharing
788// the screen looks weird.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000789// Needed because the local event monitor doesn't see the click on the menubar.
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000790
791// Gets called when the menubar is clicked.
792- (void)begunTracking:(NSNotification *)notification {
793 [self closeFolderAndStopTrackingMenus];
794}
795
796// Install the callback.
797- (void)startObservingMenubar {
798 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
799 [nc addObserver:self
800 selector:@selector(begunTracking:)
801 name:NSMenuDidBeginTrackingNotification
802 object:[NSApp mainMenu]];
803}
804
805// Remove the callback.
806- (void)stopObservingMenubar {
807 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
808 [nc removeObserver:self
809 name:NSMenuDidBeginTrackingNotification
810 object:[NSApp mainMenu]];
811}
812
813// End of menubar observation code.
814
815// Begin (or end) watching for a click outside this window. Unlike
816// normal NSWindows, bookmark folder "fake menu" windows do not become
817// key or main. Thus, traditional notification (e.g. WillResignKey)
818// won't work. Our strategy is to watch (at the app level) for a
819// "click outside" these windows to detect when they logically lose
820// focus.
821- (void)watchForExitEvent:(BOOL)watch {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000822 if (watch) {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000823 if (!exitEventTap_) {
824 exitEventTap_ = [NSEvent
825 addLocalMonitorForEventsMatchingMask:NSAnyEventMask
826 handler:^NSEvent* (NSEvent* event) {
827 if ([self isEventAnExitEvent:event])
828 [self closeFolderAndStopTrackingMenus];
829 return event;
830 }];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000831 [self startObservingMenubar];
832 }
833 } else {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000834 if (exitEventTap_) {
835 [NSEvent removeMonitor:exitEventTap_];
836 exitEventTap_ = nil;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000837 [self stopObservingMenubar];
838 }
839 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000840}
841
842// Keep the "no items" label centered in response to a frame size change.
843- (void)centerNoItemsLabel {
844 // Note that this computation is done in the parent's coordinate system,
845 // which is unflipped. Also, we want the label to be a fixed distance from
846 // the bottom, so that it slides up properly (on animating to hidden).
847 // The textfield sits in the itemcontainer, so to center it we maintain
848 // equal vertical padding on the top and bottom.
849 int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) -
850 NSHeight([[buttonView_ noItemContainer] frame])) / 2;
851 [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)];
852}
853
854// (Private)
855- (void)showBookmarkBarWithAnimation:(BOOL)animate {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000856 if (animate && stateAnimationsEnabled_) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000857 // If |-doBookmarkBarAnimation| does the animation, we're done.
858 if ([self doBookmarkBarAnimation])
859 return;
860
861 // Else fall through and do the change instantly.
862 }
863
864 // Set our height.
865 [resizeDelegate_ resizeView:[self view]
866 newHeight:[self preferredHeight]];
867
868 // Only show the divider if showing the normal bookmark bar.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000869 BOOL showsDivider = [self isInState:BookmarkBar::SHOW];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000870 [[self backgroundGradientView] setShowsDivider:showsDivider];
871
872 // Make sure we're shown.
873 [[self view] setHidden:![self isVisible]];
874
875 // Update everything else.
876 [self layoutSubviews];
877 [self frameDidChange];
878}
879
880// (Private)
881- (BOOL)doBookmarkBarAnimation {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000882 if ([self isAnimatingFromState:BookmarkBar::HIDDEN
883 toState:BookmarkBar::SHOW]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000884 [[self backgroundGradientView] setShowsDivider:YES];
885 [[self view] setHidden:NO];
886 AnimatableView* view = [self animatableView];
887 // Height takes into account the extra height we have since the toolbar
888 // only compresses when we're done.
889 [view animateToNewHeight:(bookmarks::kBookmarkBarHeight -
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100890 bookmarks::kBookmarkBarOverlap)
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000891 duration:kBookmarkBarAnimationDuration];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000892 } else if ([self isAnimatingFromState:BookmarkBar::SHOW
893 toState:BookmarkBar::HIDDEN]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000894 [[self backgroundGradientView] setShowsDivider:YES];
895 [[self view] setHidden:NO];
896 AnimatableView* view = [self animatableView];
897 [view animateToNewHeight:0
898 duration:kBookmarkBarAnimationDuration];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000899 } else if ([self isAnimatingFromState:BookmarkBar::SHOW
900 toState:BookmarkBar::DETACHED]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000901 [[self backgroundGradientView] setShowsDivider:YES];
902 [[self view] setHidden:NO];
903 AnimatableView* view = [self animatableView];
Ben Murdocheb525c52013-07-10 11:40:50 +0100904 [view animateToNewHeight:chrome::kNTPBookmarkBarHeight
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000905 duration:kBookmarkBarAnimationDuration];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000906 } else if ([self isAnimatingFromState:BookmarkBar::DETACHED
907 toState:BookmarkBar::SHOW]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000908 [[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:(bookmarks::kBookmarkBarHeight -
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100914 bookmarks::kBookmarkBarOverlap)
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000915 duration:kBookmarkBarAnimationDuration];
916 } else {
917 // Oops! An animation we don't know how to handle.
918 return NO;
919 }
920
921 return YES;
922}
923
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000924// Actually open the URL. This is the last chance for a unit test to
925// override.
926- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition {
927 OpenURLParams params(
928 url, Referrer(), disposition, content::PAGE_TRANSITION_AUTO_BOOKMARK,
929 false);
930 browser_->OpenURL(params);
931}
932
933- (void)clearMenuTagMap {
934 seedId_ = 0;
935 menuTagMap_.clear();
936}
937
938- (int)preferredHeight {
939 DCHECK(![self isAnimationRunning]);
940
941 if (!barIsEnabled_)
942 return 0;
943
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000944 switch (currentState_) {
945 case BookmarkBar::SHOW:
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000946 return bookmarks::kBookmarkBarHeight;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000947 case BookmarkBar::DETACHED:
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000948 return chrome::kNTPBookmarkBarHeight;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000949 case BookmarkBar::HIDDEN:
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000950 return 0;
951 }
952}
953
954// Recursively add the given bookmark node and all its children to
955// menu, one menu item per node.
956- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu {
957 NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child];
958 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title
959 action:nil
960 keyEquivalent:@""] autorelease];
961 [menu addItem:item];
962 [item setImage:[self faviconForNode:child]];
963 if (child->is_folder()) {
964 NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
965 [menu setSubmenu:submenu forItem:item];
966 if (!child->empty()) {
967 [self addFolderNode:child toMenu:submenu]; // potentially recursive
968 } else {
969 [self tagEmptyMenu:submenu];
970 }
971 } else {
972 [item setTarget:self];
973 [item setAction:@selector(openBookmarkMenuItem:)];
974 [item setTag:[self menuTagFromNodeId:child->id()]];
975 if (child->is_url())
976 [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:child]];
977 }
978}
979
980// Empty menus are odd; if empty, add something to look at.
981// Matches windows behavior.
982- (void)tagEmptyMenu:(NSMenu*)menu {
983 NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
984 [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title
985 action:NULL
986 keyEquivalent:@""] autorelease]];
987}
988
989// Add the children of the given bookmark node (and their children...)
990// to menu, one menu item per node.
991- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu {
992 for (int i = 0; i < node->child_count(); i++) {
993 const BookmarkNode* child = node->GetChild(i);
994 [self addNode:child toMenu:menu];
995 }
996}
997
998// Return an autoreleased NSMenu that represents the given bookmark
999// folder node.
1000- (NSMenu *)menuForFolderNode:(const BookmarkNode*)node {
1001 if (!node->is_folder())
1002 return nil;
1003 NSString* title = base::SysUTF16ToNSString(node->GetTitle());
1004 NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease];
1005 [self addFolderNode:node toMenu:menu];
1006
1007 if (![menu numberOfItems]) {
1008 [self tagEmptyMenu:menu];
1009 }
1010 return menu;
1011}
1012
1013// Return an appropriate width for the given bookmark button cell.
1014// The "+2" is needed because, sometimes, Cocoa is off by a tad.
1015// Example: for a bookmark named "Moma" or "SFGate", it is one pixel
1016// too small. For "FBL" it is 2 pixels too small.
1017// For a bookmark named "SFGateFooWoo", it is just fine.
1018- (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell {
1019 CGFloat desired = [cell cellSize].width + 2;
1020 return std::min(desired, bookmarks::kDefaultBookmarkWidth);
1021}
1022
1023- (IBAction)openBookmarkMenuItem:(id)sender {
1024 int64 tag = [self nodeIdFromMenuTag:[sender tag]];
1025 const BookmarkNode* node = bookmarkModel_->GetNodeByID(tag);
1026 WindowOpenDisposition disposition =
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001027 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001028 [self openURL:node->url() disposition:disposition];
1029}
1030
1031// For the given root node of the bookmark bar, show or hide (as
1032// appropriate) the "no items" container (text which says "bookmarks
1033// go here").
1034- (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node {
1035 BOOL hideNoItemWarning = !node->empty();
1036 [[buttonView_ noItemContainer] setHidden:hideNoItemWarning];
1037}
1038
1039// TODO(jrg): write a "build bar" so there is a nice spot for things
1040// like the contextual menu which is invoked when not over a
1041// bookmark. On Safari that menu has a "new folder" option.
1042- (void)addNodesToButtonList:(const BookmarkNode*)node {
1043 [self showOrHideNoItemContainerForNode:node];
1044
1045 CGFloat maxViewX = NSMaxX([[self view] bounds]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001046 int xOffset =
1047 bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001048
1049 // Draw the apps bookmark if needed.
1050 if (![appsPageShortcutButton_ isHidden]) {
1051 NSRect frame =
1052 [self frameForBookmarkButtonFromCell:[appsPageShortcutButton_ cell]
1053 xOffset:&xOffset];
1054 [appsPageShortcutButton_ setFrame:frame];
1055 }
1056
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001057 for (int i = 0; i < node->child_count(); i++) {
1058 const BookmarkNode* child = node->GetChild(i);
1059 BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
1060 if (NSMinX([button frame]) >= maxViewX) {
1061 [button setDelegate:nil];
1062 break;
1063 }
1064 [buttons_ addObject:button];
1065 }
1066}
1067
1068- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
1069 xOffset:(int*)xOffset {
1070 BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
1071 NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset];
1072
Ben Murdocheb525c52013-07-10 11:40:50 +01001073 base::scoped_nsobject<BookmarkButton> button(
1074 [[BookmarkButton alloc] initWithFrame:frame]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001075 DCHECK(button.get());
1076
1077 // [NSButton setCell:] warns to NOT use setCell: other than in the
1078 // initializer of a control. However, we are using a basic
1079 // NSButton whose initializer does not take an NSCell as an
1080 // object. To honor the assumed semantics, we do nothing with
1081 // NSButton between alloc/init and setCell:.
1082 [button setCell:cell];
1083 [button setDelegate:self];
1084
1085 // We cannot set the button cell's text color until it is placed in
1086 // the button (e.g. the [button setCell:cell] call right above). We
1087 // also cannot set the cell's text color until the view is added to
1088 // the hierarchy. If that second part is now true, set the color.
1089 // (If not we'll set the color on the 1st themeChanged:
1090 // notification.)
1091 ui::ThemeProvider* themeProvider = [[[self view] window] themeProvider];
1092 if (themeProvider) {
1093 NSColor* color =
Ben Murdochbb1529c2013-08-08 10:24:53 +01001094 themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001095 [cell setTextColor:color];
1096 }
1097
1098 if (node->is_folder()) {
1099 [button setTarget:self];
1100 [button setAction:@selector(openBookmarkFolderFromButton:)];
1101 [[button draggableButton] setActsOnMouseDown:YES];
1102 // If it has a title, and it will be truncated, show full title in
1103 // tooltip.
1104 NSString* title = base::SysUTF16ToNSString(node->GetTitle());
1105 if ([title length] &&
1106 [[button cell] cellSize].width > bookmarks::kDefaultBookmarkWidth) {
1107 [button setToolTip:title];
1108 }
1109 } else {
1110 // Make the button do something
1111 [button setTarget:self];
1112 [button setAction:@selector(openBookmark:)];
1113 if (node->is_url())
1114 [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
1115 }
1116 return [[button.get() retain] autorelease];
1117}
1118
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001119// Add bookmark buttons to the view only if they are completely
1120// visible and don't overlap the "other bookmarks". Remove buttons
1121// which are clipped. Called when building the bookmark bar the first time.
1122- (void)addButtonsToView {
1123 displayedButtonCount_ = 0;
1124 NSMutableArray* buttons = [self buttons];
1125 for (NSButton* button in buttons) {
1126 if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) -
1127 bookmarks::kBookmarkHorizontalPadding))
1128 break;
1129 [buttonView_ addSubview:button];
1130 ++displayedButtonCount_;
1131 }
1132 NSUInteger removalCount =
1133 [buttons count] - (NSUInteger)displayedButtonCount_;
1134 if (removalCount > 0) {
1135 NSRange removalRange = NSMakeRange(displayedButtonCount_, removalCount);
1136 [buttons removeObjectsInRange:removalRange];
1137 }
1138}
1139
1140// Shows or hides the Other Bookmarks button as appropriate, and returns
1141// whether it ended up visible.
1142- (BOOL)setOtherBookmarksButtonVisibility {
1143 if (!otherBookmarksButton_.get())
1144 return NO;
1145
1146 BOOL visible = ![otherBookmarksButton_ bookmarkNode]->empty();
1147 [otherBookmarksButton_ setHidden:!visible];
1148 return visible;
1149}
1150
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001151// Shows or hides the Apps button as appropriate, and returns whether it ended
1152// up visible.
1153- (BOOL)setAppsPageShortcutButtonVisibility {
1154 if (!appsPageShortcutButton_.get())
1155 return NO;
1156
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01001157 BOOL visible = bookmarkModel_->loaded() &&
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001158 chrome::ShouldShowAppsShortcutInBookmarkBar(browser_->profile());
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001159 [appsPageShortcutButton_ setHidden:!visible];
1160 return visible;
1161}
1162
1163// Creates a bookmark bar button that does not correspond to a regular bookmark
1164// or folder. It is used by the "Other Bookmarks" and the "Apps" buttons.
1165- (BookmarkButton*)customBookmarkButtonForCell:(NSCell*)cell {
1166 BookmarkButton* button = [[BookmarkButton alloc] init];
1167 [[button draggableButton] setDraggable:NO];
1168 [[button draggableButton] setActsOnMouseDown:YES];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001169 [button setCell:cell];
1170 [button setDelegate:self];
1171 [button setTarget:self];
1172 // Make sure this button, like all other BookmarkButtons, lives
1173 // until the end of the current event loop.
1174 [[button retain] autorelease];
1175 return button;
1176}
1177
1178// Creates the button for "Other Bookmarks", but does not position it.
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001179- (void)createOtherBookmarksButton {
1180 // Can't create this until the model is loaded, but only need to
1181 // create it once.
1182 if (otherBookmarksButton_.get()) {
1183 [self setOtherBookmarksButtonVisibility];
1184 return;
1185 }
1186
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001187 NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001188 otherBookmarksButton_.reset([self customBookmarkButtonForCell:cell]);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001189 // Peg at right; keep same height as bar.
1190 [otherBookmarksButton_ setAutoresizingMask:(NSViewMinXMargin)];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001191 [otherBookmarksButton_ setAction:@selector(openBookmarkFolderFromButton:)];
1192 view_id_util::SetID(otherBookmarksButton_.get(), VIEW_ID_OTHER_BOOKMARKS);
1193 [buttonView_ addSubview:otherBookmarksButton_.get()];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001194
1195 [self setOtherBookmarksButtonVisibility];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001196}
1197
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001198// Creates the button for "Apps", but does not position it.
1199- (void)createAppsPageShortcutButton {
1200 // Can't create this until the model is loaded, but only need to
1201 // create it once.
1202 if (appsPageShortcutButton_.get()) {
1203 [self setAppsPageShortcutButtonVisibility];
1204 return;
1205 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001206
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001207 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
1208 NSString* text = l10n_util::GetNSString(IDS_BOOKMARK_BAR_APPS_SHORTCUT_NAME);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001209 NSImage* image = rb.GetNativeImageNamed(
1210 IDR_BOOKMARK_BAR_APPS_SHORTCUT).ToNSImage();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001211 NSCell* cell = [self cellForCustomButtonWithText:text
1212 image:image];
1213 appsPageShortcutButton_.reset([self customBookmarkButtonForCell:cell]);
1214 [[appsPageShortcutButton_ draggableButton] setActsOnMouseDown:NO];
1215 [appsPageShortcutButton_ setAction:@selector(openAppsPage:)];
1216 NSString* tooltip =
1217 l10n_util::GetNSString(IDS_BOOKMARK_BAR_APPS_SHORTCUT_TOOLTIP);
1218 [appsPageShortcutButton_ setToolTip:tooltip];
1219 [buttonView_ addSubview:appsPageShortcutButton_.get()];
1220
1221 [self setAppsPageShortcutButtonVisibility];
1222}
1223
1224- (void)openAppsPage:(id)sender {
1225 WindowOpenDisposition disposition =
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01001226 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001227 [self openURL:GURL(chrome::kChromeUIAppsURL) disposition:disposition];
1228 bookmark_utils::RecordAppsPageOpen([self bookmarkLaunchLocation]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001229}
1230
1231// To avoid problems with sync, changes that may impact the current
1232// bookmark (e.g. deletion) make sure context menus are closed. This
1233// prevents deleting a node which no longer exists.
1234- (void)cancelMenuTracking {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001235 [contextMenuController_ cancelTracking];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001236}
1237
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001238- (void)moveToState:(BookmarkBar::State)nextState
1239 withAnimation:(BOOL)animate {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001240 BOOL isAnimationRunning = [self isAnimationRunning];
1241
1242 // No-op if the next state is the same as the "current" one, subject to the
1243 // following conditions:
1244 // - no animation is running; or
1245 // - an animation is running and |animate| is YES ([*] if it's NO, we'd want
1246 // to cancel the animation and jump to the final state).
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001247 if ((nextState == currentState_) && (!isAnimationRunning || animate))
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001248 return;
1249
1250 // If an animation is running, we want to finalize it. Otherwise we'd have to
1251 // be able to animate starting from the middle of one type of animation. We
1252 // assume that animations that we know about can be "reversed".
1253 if (isAnimationRunning) {
1254 // Don't cancel if we're going to reverse the animation.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001255 if (nextState != lastState_) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001256 [self stopCurrentAnimation];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001257 [self finalizeState];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001258 }
1259
1260 // If we're in case [*] above, we can stop here.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001261 if (nextState == currentState_)
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001262 return;
1263 }
1264
1265 // Now update with the new state change.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001266 lastState_ = currentState_;
1267 currentState_ = nextState;
1268 isAnimationRunning_ = YES;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001269
1270 // Animate only if told to and if bar is enabled.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001271 if (animate && stateAnimationsEnabled_ && barIsEnabled_) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001272 [self closeAllBookmarkFolders];
1273 // Take care of any animation cases we know how to handle.
1274
1275 // We know how to handle hidden <-> normal, normal <-> detached....
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001276 if ([self isAnimatingBetweenState:BookmarkBar::HIDDEN
1277 andState:BookmarkBar::SHOW] ||
1278 [self isAnimatingBetweenState:BookmarkBar::SHOW
1279 andState:BookmarkBar::DETACHED]) {
1280 [delegate_ bookmarkBar:self
1281 willAnimateFromState:lastState_
1282 toState:currentState_];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001283 [self showBookmarkBarWithAnimation:YES];
1284 return;
1285 }
1286
1287 // If we ever need any other animation cases, code would go here.
1288 // Let any animation cases which we don't know how to handle fall through to
1289 // the unanimated case.
1290 }
1291
1292 // Just jump to the state.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001293 [self finalizeState];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001294}
1295
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001296// N.B.: |-moveToState:...| will check if this should be a no-op or not.
1297- (void)updateState:(BookmarkBar::State)newState
1298 changeType:(BookmarkBar::AnimateChangeType)changeType {
1299 BOOL animate = changeType == BookmarkBar::ANIMATE_STATE_CHANGE &&
1300 stateAnimationsEnabled_;
1301 [self moveToState:newState withAnimation:animate];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001302}
1303
1304// (Private)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001305- (void)finalizeState {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001306 // We promise that our delegate that the variables will be finalized before
1307 // the call to |-bookmarkBar:didChangeFromState:toState:|.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001308 BookmarkBar::State oldState = lastState_;
1309 lastState_ = currentState_;
1310 isAnimationRunning_ = NO;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001311
1312 // Notify our delegate.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001313 [delegate_ bookmarkBar:self
1314 didChangeFromState:oldState
1315 toState:currentState_];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001316
1317 // Update ourselves visually.
1318 [self updateVisibility];
1319}
1320
1321// (Private)
1322- (void)stopCurrentAnimation {
1323 [[self animatableView] stopAnimation];
1324}
1325
1326// Delegate method for |AnimatableView| (a superclass of
1327// |BookmarkBarToolbarView|).
1328- (void)animationDidEnd:(NSAnimation*)animation {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001329 [self finalizeState];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001330}
1331
1332- (void)reconfigureBookmarkBar {
1333 [self redistributeButtonsOnBarAsNeeded];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001334 [self positionRightSideButtons];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001335 [self configureOffTheSideButtonContentsAndVisibility];
1336 [self centerNoItemsLabel];
1337}
1338
1339// Determine if the given |view| can completely fit within the constraint of
1340// maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum
1341// width. If the minimum width is not achievable then hide the view. Return YES
1342// if the view was hidden.
1343- (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX {
1344 BOOL wasHidden = NO;
1345 // See if the view needs to be narrowed.
1346 NSRect frame = [view frame];
1347 if (NSMaxX(frame) > maxViewX) {
1348 // Resize if more than 30 pixels are showing, otherwise hide.
1349 if (NSMinX(frame) + 30.0 < maxViewX) {
1350 frame.size.width = maxViewX - NSMinX(frame);
1351 [view setFrame:frame];
1352 } else {
1353 [view setHidden:YES];
1354 wasHidden = YES;
1355 }
1356 }
1357 return wasHidden;
1358}
1359
1360// Bookmark button menu items that open a new window (e.g., open in new window,
1361// open in incognito, edit, etc.) cause us to lose a mouse-exited event
1362// on the button, which leaves it in a hover state.
1363// Since the showsBorderOnlyWhileMouseInside uses a tracking area, simple
1364// tricks (e.g. sending an extra mouseExited: to the button) don't
1365// fix the problem.
1366// http://crbug.com/129338
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001367- (void)unhighlightBookmark:(const BookmarkNode*)node {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001368 // Only relevant if context menu was opened from a button on the
1369 // bookmark bar.
1370 const BookmarkNode* parent = node->parent();
1371 BookmarkNode::Type parentType = parent->type();
1372 if (parentType == BookmarkNode::BOOKMARK_BAR) {
1373 int index = parent->GetIndexOf(node);
1374 if ((index >= 0) && (static_cast<NSUInteger>(index) < [buttons_ count])) {
1375 NSButton* button =
1376 [buttons_ objectAtIndex:static_cast<NSUInteger>(index)];
1377 if ([button showsBorderOnlyWhileMouseInside]) {
1378 [button setShowsBorderOnlyWhileMouseInside:NO];
1379 [button setShowsBorderOnlyWhileMouseInside:YES];
1380 }
1381 }
1382 }
1383}
1384
1385
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001386// Adjust the horizontal width, x position and the visibility of the "For quick
1387// access" text field and "Import bookmarks..." button based on the current
1388// width of the containing |buttonView_| (which is affected by window width).
1389- (void)adjustNoItemContainerForMaxX:(CGFloat)maxViewX {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001390 if (![[buttonView_ noItemContainer] isHidden]) {
1391 // Reset initial frames for the two items, then adjust as necessary.
1392 NSTextField* noItemTextfield = [buttonView_ noItemTextfield];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001393 NSRect noItemsRect = originalNoItemsRect_;
1394 NSRect importBookmarksRect = originalImportBookmarksRect_;
1395 if (![appsPageShortcutButton_ isHidden]) {
1396 float width = NSWidth([appsPageShortcutButton_ frame]);
1397 noItemsRect.origin.x += width;
1398 importBookmarksRect.origin.x += width;
1399 }
1400 [noItemTextfield setFrame:noItemsRect];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001401 [noItemTextfield setHidden:NO];
1402 NSButton* importBookmarksButton = [buttonView_ importBookmarksButton];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001403 [importBookmarksButton setFrame:importBookmarksRect];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001404 [importBookmarksButton setHidden:NO];
1405 // Check each to see if they need to be shrunk or hidden.
1406 if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX])
1407 [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX];
1408 }
1409}
1410
1411// Scans through all buttons from left to right, calculating from scratch where
1412// they should be based on the preceding widths, until it finds the one
1413// requested.
1414// Returns NSZeroRect if there is no such button in the bookmark bar.
1415// Enables you to work out where a button will end up when it is done animating.
1416- (NSRect)finalRectOfButton:(BookmarkButton*)wantedButton {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001417 CGFloat left = bookmarks::kBookmarkLeftMargin;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001418 NSRect buttonFrame = NSZeroRect;
1419
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001420 // Draw the apps bookmark if needed.
1421 if (![appsPageShortcutButton_ isHidden]) {
1422 left = NSMaxX([appsPageShortcutButton_ frame]) +
1423 bookmarks::kBookmarkHorizontalPadding;
1424 }
1425
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001426 for (NSButton* button in buttons_.get()) {
1427 // Hidden buttons get no space.
1428 if ([button isHidden])
1429 continue;
1430 buttonFrame = [button frame];
1431 buttonFrame.origin.x = left;
1432 left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding;
1433 if (button == wantedButton)
1434 return buttonFrame;
1435 }
1436 return NSZeroRect;
1437}
1438
1439// Calculates the final position of the last button in the bar.
1440// We can't just use [[self buttons] lastObject] frame] because the button
1441// may be animating currently.
1442- (NSRect)finalRectOfLastButton {
1443 return [self finalRectOfButton:[[self buttons] lastObject]];
1444}
1445
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001446- (CGFloat)buttonViewMaxXWithOffTheSideButtonIsVisible:(BOOL)visible {
1447 CGFloat maxViewX = NSMaxX([buttonView_ bounds]);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001448 // If necessary, pull in the width to account for the Other Bookmarks button.
1449 if ([self setOtherBookmarksButtonVisibility]) {
1450 maxViewX = [otherBookmarksButton_ frame].origin.x -
1451 bookmarks::kBookmarkRightMargin;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001452 }
1453
1454 [self positionRightSideButtons];
1455 // If we're already overflowing, then we need to account for the chevron.
1456 if (visible) {
1457 maxViewX =
1458 [offTheSideButton_ frame].origin.x - bookmarks::kBookmarkRightMargin;
1459 }
1460
1461 return maxViewX;
1462}
1463
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001464- (void)redistributeButtonsOnBarAsNeeded {
1465 const BookmarkNode* node = bookmarkModel_->bookmark_bar_node();
1466 NSInteger barCount = node->child_count();
1467
1468 // Determine the current maximum extent of the visible buttons.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001469 [self positionRightSideButtons];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001470 BOOL offTheSideButtonVisible = (barCount > displayedButtonCount_);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001471 CGFloat maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001472 offTheSideButtonVisible];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001473
1474 // As a result of pasting or dragging, the bar may now have more buttons
1475 // than will fit so remove any which overflow. They will be shown in
1476 // the off-the-side folder.
1477 while (displayedButtonCount_ > 0) {
1478 BookmarkButton* button = [buttons_ lastObject];
1479 if (NSMaxX([self finalRectOfLastButton]) < maxViewX)
1480 break;
1481 [buttons_ removeLastObject];
1482 [button setDelegate:nil];
1483 [button removeFromSuperview];
1484 --displayedButtonCount_;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001485 // Account for the fact that the chevron might now be visible.
1486 if (!offTheSideButtonVisible) {
1487 offTheSideButtonVisible = YES;
1488 maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:YES];
1489 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001490 }
1491
1492 // As a result of cutting, deleting and dragging, the bar may now have room
1493 // for more buttons.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001494 int xOffset;
1495 if (displayedButtonCount_ > 0) {
1496 xOffset = NSMaxX([self finalRectOfLastButton]) +
1497 bookmarks::kBookmarkHorizontalPadding;
1498 } else if (![appsPageShortcutButton_ isHidden]) {
1499 xOffset = NSMaxX([appsPageShortcutButton_ frame]) +
1500 bookmarks::kBookmarkHorizontalPadding;
1501 } else {
1502 xOffset = bookmarks::kBookmarkLeftMargin -
1503 bookmarks::kBookmarkHorizontalPadding;
1504 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001505 for (int i = displayedButtonCount_; i < barCount; ++i) {
1506 const BookmarkNode* child = node->GetChild(i);
1507 BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
1508 // If we're testing against the last possible button then account
1509 // for the chevron no longer needing to be shown.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001510 if (i == barCount - 1)
1511 maxViewX = [self buttonViewMaxXWithOffTheSideButtonIsVisible:NO];
1512 if (NSMaxX([button frame]) > maxViewX) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001513 [button setDelegate:nil];
1514 break;
1515 }
1516 ++displayedButtonCount_;
1517 [buttons_ addObject:button];
1518 [buttonView_ addSubview:button];
1519 }
1520
1521 // While we're here, adjust the horizontal width and the visibility
1522 // of the "For quick access" and "Import bookmarks..." text fields.
1523 if (![buttons_ count])
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001524 [self adjustNoItemContainerForMaxX:maxViewX];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001525}
1526
1527#pragma mark Private Methods Exposed for Testing
1528
1529- (BookmarkBarView*)buttonView {
1530 return buttonView_;
1531}
1532
1533- (NSMutableArray*)buttons {
1534 return buttons_.get();
1535}
1536
1537- (NSButton*)offTheSideButton {
1538 return offTheSideButton_;
1539}
1540
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001541- (NSButton*)appsPageShortcutButton {
1542 return appsPageShortcutButton_;
1543}
1544
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001545- (BOOL)offTheSideButtonIsHidden {
1546 return [offTheSideButton_ isHidden];
1547}
1548
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001549- (BOOL)appsPageShortcutButtonIsHidden {
1550 return [appsPageShortcutButton_ isHidden];
1551}
1552
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001553- (BookmarkButton*)otherBookmarksButton {
1554 return otherBookmarksButton_.get();
1555}
1556
1557- (BookmarkBarFolderController*)folderController {
1558 return folderController_;
1559}
1560
1561- (id)folderTarget {
1562 return folderTarget_.get();
1563}
1564
1565- (int)displayedButtonCount {
1566 return displayedButtonCount_;
1567}
1568
1569// Delete all buttons (bookmarks, chevron, "other bookmarks") from the
1570// bookmark bar; reset knowledge of bookmarks.
1571- (void)clearBookmarkBar {
1572 for (BookmarkButton* button in buttons_.get()) {
1573 [button setDelegate:nil];
1574 [button removeFromSuperview];
1575 }
1576 [buttons_ removeAllObjects];
1577 [self clearMenuTagMap];
1578 displayedButtonCount_ = 0;
1579
1580 // Make sure there are no stale pointers in the pasteboard. This
1581 // can be important if a bookmark is deleted (via bookmark sync)
1582 // while in the middle of a drag. The "drag completed" code
1583 // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is
1584 // careful enough to bail if there is no data found at "drop" time.
1585 //
1586 // Unfortunately the clearContents selector is 10.6 only. The best
1587 // we can do is make sure something else is present in place of the
1588 // stale bookmark.
1589 NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
1590 [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self];
1591 [pboard setString:@"" forType:NSStringPboardType];
1592}
1593
1594// Return an autoreleased NSCell suitable for a bookmark button.
1595// TODO(jrg): move much of the cell config into the BookmarkButtonCell class.
1596- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node {
1597 NSImage* image = node ? [self faviconForNode:node] : nil;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001598 BookmarkButtonCell* cell =
1599 [BookmarkButtonCell buttonCellForNode:node
1600 text:nil
1601 image:image
1602 menuController:contextMenuController_];
1603 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
1604
1605 // Note: a quirk of setting a cell's text color is that it won't work
1606 // until the cell is associated with a button, so we can't theme the cell yet.
1607
1608 return cell;
1609}
1610
1611// Return an autoreleased NSCell suitable for a special button displayed on the
1612// bookmark bar that is not attached to any bookmark node.
1613// TODO(jrg): move much of the cell config into the BookmarkButtonCell class.
1614- (BookmarkButtonCell*)cellForCustomButtonWithText:(NSString*)text
1615 image:(NSImage*)image {
1616 BookmarkButtonCell* cell =
1617 [BookmarkButtonCell buttonCellWithText:text
1618 image:image
1619 menuController:contextMenuController_];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001620 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
1621
1622 // Note: a quirk of setting a cell's text color is that it won't work
1623 // until the cell is associated with a button, so we can't theme the cell yet.
1624
1625 return cell;
1626}
1627
1628// Returns a frame appropriate for the given bookmark cell, suitable
1629// for creating an NSButton that will contain it. |xOffset| is the X
1630// offset for the frame; it is increased to be an appropriate X offset
1631// for the next button.
1632- (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell
1633 xOffset:(int*)xOffset {
1634 DCHECK(xOffset);
1635 NSRect bounds = [buttonView_ bounds];
1636 bounds.size.height = bookmarks::kBookmarkButtonHeight;
1637
1638 NSRect frame = NSInsetRect(bounds,
1639 bookmarks::kBookmarkHorizontalPadding,
1640 bookmarks::kBookmarkVerticalPadding);
1641 frame.size.width = [self widthForBookmarkButtonCell:cell];
1642
1643 // Add an X offset based on what we've already done
1644 frame.origin.x += *xOffset;
1645
1646 // And up the X offset for next time.
1647 *xOffset = NSMaxX(frame);
1648
1649 return frame;
1650}
1651
1652// A bookmark button's contents changed. Check for growth
1653// (e.g. increase the width up to the maximum). If we grew, move
1654// other bookmark buttons over.
1655- (void)checkForBookmarkButtonGrowth:(NSButton*)changedButton {
1656 NSRect frame = [changedButton frame];
1657 CGFloat desiredSize = [self widthForBookmarkButtonCell:[changedButton cell]];
1658 CGFloat delta = desiredSize - frame.size.width;
1659 if (delta) {
1660 frame.size.width = desiredSize;
1661 [changedButton setFrame:frame];
1662 for (NSButton* button in buttons_.get()) {
1663 NSRect buttonFrame = [button frame];
1664 if (buttonFrame.origin.x > frame.origin.x) {
1665 buttonFrame.origin.x += delta;
1666 [button setFrame:buttonFrame];
1667 }
1668 }
1669 }
1670 // We may have just crossed a threshold to enable the off-the-side
1671 // button.
1672 [self configureOffTheSideButtonContentsAndVisibility];
1673}
1674
1675// Called when our controlled frame has changed size.
1676- (void)frameDidChange {
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01001677 if (!bookmarkModel_->loaded())
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001678 return;
1679 [self updateTheme:[[[self view] window] themeProvider]];
1680 [self reconfigureBookmarkBar];
1681}
1682
1683// Given a NSMenuItem tag, return the appropriate bookmark node id.
1684- (int64)nodeIdFromMenuTag:(int32)tag {
1685 return menuTagMap_[tag];
1686}
1687
1688// Create and return a new tag for the given node id.
1689- (int32)menuTagFromNodeId:(int64)menuid {
1690 int tag = seedId_++;
1691 menuTagMap_[tag] = menuid;
1692 return tag;
1693}
1694
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001695// Adapt appearance of buttons to the current theme. Called after
1696// theme changes, or when our view is added to the view hierarchy.
1697// Oddly, the view pings us instead of us pinging our view. This is
1698// because our trigger is an [NSView viewWillMoveToWindow:], which the
1699// controller doesn't normally know about. Otherwise we don't have
1700// access to the theme before we know what window we will be on.
1701- (void)updateTheme:(ui::ThemeProvider*)themeProvider {
1702 if (!themeProvider)
1703 return;
1704 NSColor* color =
Ben Murdochbb1529c2013-08-08 10:24:53 +01001705 themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001706 for (BookmarkButton* button in buttons_.get()) {
1707 BookmarkButtonCell* cell = [button cell];
1708 [cell setTextColor:color];
1709 }
1710 [[otherBookmarksButton_ cell] setTextColor:color];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001711 [[appsPageShortcutButton_ cell] setTextColor:color];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001712}
1713
1714// Return YES if the event indicates an exit from the bookmark bar
1715// folder menus. E.g. "click outside" of the area we are watching.
1716// At this time we are watching the area that includes all popup
1717// bookmark folder windows.
1718- (BOOL)isEventAnExitEvent:(NSEvent*)event {
1719 NSWindow* eventWindow = [event window];
1720 NSWindow* myWindow = [[self view] window];
1721 switch ([event type]) {
1722 case NSLeftMouseDown:
1723 case NSRightMouseDown:
1724 // If the click is in my window but NOT in the bookmark bar, consider
1725 // it a click 'outside'. Clicks directly on an active button (i.e. one
1726 // that is a folder and for which its folder menu is showing) are 'in'.
1727 // All other clicks on the bookmarks bar are counted as 'outside'
1728 // because they should close any open bookmark folder menu.
1729 if (eventWindow == myWindow) {
1730 NSView* hitView =
1731 [[eventWindow contentView] hitTest:[event locationInWindow]];
1732 if (hitView == [folderController_ parentButton])
1733 return NO;
1734 if (![hitView isDescendantOf:[self view]] || hitView == buttonView_)
1735 return YES;
1736 }
1737 // If a click in a bookmark bar folder window and that isn't
1738 // one of my bookmark bar folders, YES is click outside.
1739 if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow
1740 class]]) {
1741 return YES;
1742 }
1743 break;
1744 case NSKeyDown: {
1745 // Event hooks often see the same keydown event twice due to the way key
1746 // events get dispatched and redispatched, so ignore if this keydown
1747 // event has the EXACT same timestamp as the previous keydown.
1748 static NSTimeInterval lastKeyDownEventTime;
1749 NSTimeInterval thisTime = [event timestamp];
1750 if (lastKeyDownEventTime != thisTime) {
1751 lastKeyDownEventTime = thisTime;
1752 if ([event modifierFlags] & NSCommandKeyMask)
1753 return YES;
1754 else if (folderController_)
1755 return [folderController_ handleInputText:[event characters]];
1756 }
1757 return NO;
1758 }
1759 case NSKeyUp:
1760 return NO;
1761 case NSLeftMouseDragged:
1762 // We can get here with the following sequence:
1763 // - open a bookmark folder
1764 // - right-click (and unclick) on it to open context menu
1765 // - move mouse to window titlebar then click-drag it by the titlebar
1766 // http://crbug.com/49333
1767 return NO;
1768 default:
1769 break;
1770 }
1771 return NO;
1772}
1773
1774#pragma mark Drag & Drop
1775
1776// Find something like std::is_between<T>? I can't believe one doesn't exist.
1777static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
1778 return ((value >= low) && (value <= high));
1779}
1780
1781// Return the proposed drop target for a hover open button from the
1782// given array, or nil if none. We use this for distinguishing
1783// between a hover-open candidate or drop-indicator draw.
1784// Helper for buttonForDroppingOnAtPoint:.
1785// Get UI review on "middle half" ness.
1786// http://crbug.com/36276
1787- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point
1788 fromArray:(NSArray*)array {
1789 for (BookmarkButton* button in array) {
1790 // Hidden buttons can overlap valid visible buttons, just ignore.
1791 if ([button isHidden])
1792 continue;
1793 // Break early if we've gone too far.
1794 if ((NSMinX([button frame]) > point.x) || (![button superview]))
1795 return nil;
1796 // Careful -- this only applies to the bar with horiz buttons.
1797 // Intentionally NOT using NSPointInRect() so that scrolling into
1798 // a submenu doesn't cause it to be closed.
1799 if (ValueInRangeInclusive(NSMinX([button frame]),
1800 point.x,
1801 NSMaxX([button frame]))) {
1802 // Over a button but let's be a little more specific (make sure
1803 // it's over the middle half, not just over it).
1804 NSRect frame = [button frame];
1805 NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0);
1806 if (ValueInRangeInclusive(NSMinX(middleHalfOfButton),
1807 point.x,
1808 NSMaxX(middleHalfOfButton))) {
1809 // It makes no sense to drop on a non-folder; there is no hover.
1810 if (![button isFolder])
1811 return nil;
1812 // Got it!
1813 return button;
1814 } else {
1815 // Over a button but not over the middle half.
1816 return nil;
1817 }
1818 }
1819 }
1820 // Not hovering over a button.
1821 return nil;
1822}
1823
1824// Return the proposed drop target for a hover open button, or nil if
1825// none. Works with both the bookmark buttons and the "Other
1826// Bookmarks" button. Point is in [self view] coordinates.
1827- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
1828 point = [[self view] convertPoint:point
1829 fromView:[[[self view] window] contentView]];
1830
1831 // If there's a hover button, return it if the point is within its bounds.
1832 // Since the logic in -buttonForDroppingOnAtPoint:fromArray: only matches a
1833 // button when the point is over the middle half, this is needed to prevent
1834 // the button's folder being closed if the mouse temporarily leaves the
1835 // middle half but is still within the button bounds.
1836 if (hoverButton_ && NSPointInRect(point, [hoverButton_ frame]))
1837 return hoverButton_.get();
1838
1839 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point
1840 fromArray:buttons_.get()];
1841 // One more chance -- try "Other Bookmarks" and "off the side" (if visible).
1842 // This is different than BookmarkBarFolderController.
1843 if (!button) {
1844 NSMutableArray* array = [NSMutableArray array];
1845 if (![self offTheSideButtonIsHidden])
1846 [array addObject:offTheSideButton_];
1847 [array addObject:otherBookmarksButton_];
1848 button = [self buttonForDroppingOnAtPoint:point
1849 fromArray:array];
1850 }
1851 return button;
1852}
1853
1854- (int)indexForDragToPoint:(NSPoint)point {
1855 // TODO(jrg): revisit position info based on UI team feedback.
1856 // dropLocation is in bar local coordinates.
1857 NSPoint dropLocation =
1858 [[self view] convertPoint:point
1859 fromView:[[[self view] window] contentView]];
1860 BookmarkButton* buttonToTheRightOfDraggedButton = nil;
1861 for (BookmarkButton* button in buttons_.get()) {
1862 CGFloat midpoint = NSMidX([button frame]);
1863 if (dropLocation.x <= midpoint) {
1864 buttonToTheRightOfDraggedButton = button;
1865 break;
1866 }
1867 }
1868 if (buttonToTheRightOfDraggedButton) {
1869 const BookmarkNode* afterNode =
1870 [buttonToTheRightOfDraggedButton bookmarkNode];
1871 DCHECK(afterNode);
1872 int index = afterNode->parent()->GetIndexOf(afterNode);
1873 // Make sure we don't get confused by buttons which aren't visible.
1874 return std::min(index, displayedButtonCount_);
1875 }
1876
1877 // If nothing is to my right I am at the end!
1878 return displayedButtonCount_;
1879}
1880
1881// TODO(mrossetti,jrg): Yet more duplicated code.
1882// http://crbug.com/35966
1883- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
1884 to:(NSPoint)point
1885 copy:(BOOL)copy {
1886 DCHECK(sourceNode);
1887 // Drop destination.
1888 const BookmarkNode* destParent = NULL;
1889 int destIndex = 0;
1890
1891 // First check if we're dropping on a button. If we have one, and
1892 // it's a folder, drop in it.
1893 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1894 if ([button isFolder]) {
1895 destParent = [button bookmarkNode];
1896 // Drop it at the end.
1897 destIndex = [button bookmarkNode]->child_count();
1898 } else {
1899 // Else we're dropping somewhere on the bar, so find the right spot.
1900 destParent = bookmarkModel_->bookmark_bar_node();
1901 destIndex = [self indexForDragToPoint:point];
1902 }
1903
1904 // Be sure we don't try and drop a folder into itself.
1905 if (sourceNode != destParent) {
1906 if (copy)
1907 bookmarkModel_->Copy(sourceNode, destParent, destIndex);
1908 else
1909 bookmarkModel_->Move(sourceNode, destParent, destIndex);
1910 }
1911
1912 [self closeFolderAndStopTrackingMenus];
1913
1914 // Movement of a node triggers observers (like us) to rebuild the
1915 // bar so we don't have to do so explicitly.
1916
1917 return YES;
1918}
1919
1920- (void)draggingEnded:(id<NSDraggingInfo>)info {
1921 [self closeFolderAndStopTrackingMenus];
1922 [[BookmarkButton draggedButton] setHidden:NO];
1923 [self resetAllButtonPositionsWithAnimation:YES];
1924}
1925
1926// Set insertionPos_ and hasInsertionPos_, and make insertion space for a
1927// hypothetical drop with the new button having a left edge of |where|.
1928// Gets called only by our view.
1929- (void)setDropInsertionPos:(CGFloat)where {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001930 if (!hasInsertionPos_ || where != insertionPos_) {
1931 insertionPos_ = where;
1932 hasInsertionPos_ = YES;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001933 CGFloat left = [appsPageShortcutButton_ isHidden] ?
1934 bookmarks::kBookmarkLeftMargin :
1935 NSMaxX([appsPageShortcutButton_ frame]) +
1936 bookmarks::kBookmarkHorizontalPadding;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001937 CGFloat paddingWidth = bookmarks::kDefaultBookmarkWidth;
1938 BookmarkButton* draggedButton = [BookmarkButton draggedButton];
1939 if (draggedButton) {
1940 paddingWidth = std::min(bookmarks::kDefaultBookmarkWidth,
1941 NSWidth([draggedButton frame]));
1942 }
1943 // Put all the buttons where they belong, with all buttons to the right
1944 // of the insertion point shuffling right to make space for it.
1945 for (NSButton* button in buttons_.get()) {
1946 // Hidden buttons get no space.
1947 if ([button isHidden])
1948 continue;
1949 NSRect buttonFrame = [button frame];
1950 buttonFrame.origin.x = left;
1951 // Update "left" for next time around.
1952 left += buttonFrame.size.width;
1953 if (left > insertionPos_)
1954 buttonFrame.origin.x += paddingWidth;
1955 left += bookmarks::kBookmarkHorizontalPadding;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001956 if (innerContentAnimationsEnabled_)
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001957 [[button animator] setFrame:buttonFrame];
1958 else
1959 [button setFrame:buttonFrame];
1960 }
1961 }
1962}
1963
1964// Put all visible bookmark bar buttons in their normal locations, either with
1965// or without animation according to the |animate| flag.
1966// This is generally useful, so is called from various places internally.
1967- (void)resetAllButtonPositionsWithAnimation:(BOOL)animate {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001968
1969 // Position the apps bookmark if needed.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001970 CGFloat left = bookmarks::kBookmarkLeftMargin;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001971 if (![appsPageShortcutButton_ isHidden]) {
1972 int xOffset =
1973 bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding;
1974 NSRect frame =
1975 [self frameForBookmarkButtonFromCell:[appsPageShortcutButton_ cell]
1976 xOffset:&xOffset];
1977 [appsPageShortcutButton_ setFrame:frame];
1978 left = xOffset + bookmarks::kBookmarkHorizontalPadding;
1979 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001980 animate &= innerContentAnimationsEnabled_;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001981
1982 for (NSButton* button in buttons_.get()) {
1983 // Hidden buttons get no space.
1984 if ([button isHidden])
1985 continue;
1986 NSRect buttonFrame = [button frame];
1987 buttonFrame.origin.x = left;
1988 left += buttonFrame.size.width + bookmarks::kBookmarkHorizontalPadding;
1989 if (animate)
1990 [[button animator] setFrame:buttonFrame];
1991 else
1992 [button setFrame:buttonFrame];
1993 }
1994}
1995
1996// Clear insertion flag, remove insertion space and put all visible bookmark
1997// bar buttons in their normal locations.
1998// Gets called only by our view.
1999- (void)clearDropInsertionPos {
2000 if (hasInsertionPos_) {
2001 hasInsertionPos_ = NO;
2002 [self resetAllButtonPositionsWithAnimation:YES];
2003 }
2004}
2005
2006#pragma mark Bridge Notification Handlers
2007
2008// TODO(jrg): for now this is brute force.
2009- (void)loaded:(BookmarkModel*)model {
2010 DCHECK(model == bookmarkModel_);
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01002011 if (!model->loaded())
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002012 return;
2013
2014 // If this is a rebuild request while we have a folder open, close it.
2015 // TODO(mrossetti): Eliminate the need for this because it causes the folder
2016 // menu to disappear after a cut/copy/paste/delete change.
2017 // See: http://crbug.com/36614
2018 if (folderController_)
2019 [self closeAllBookmarkFolders];
2020
2021 // Brute force nuke and build.
2022 savedFrameWidth_ = NSWidth([[self view] frame]);
2023 const BookmarkNode* node = model->bookmark_bar_node();
2024 [self clearBookmarkBar];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002025 [self createAppsPageShortcutButton];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002026 [self addNodesToButtonList:node];
2027 [self createOtherBookmarksButton];
2028 [self updateTheme:[[[self view] window] themeProvider]];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002029 [self positionRightSideButtons];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002030 [self addButtonsToView];
2031 [self configureOffTheSideButtonContentsAndVisibility];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002032 [self reconfigureBookmarkBar];
2033}
2034
2035- (void)beingDeleted:(BookmarkModel*)model {
2036 // The browser may be being torn down; little is safe to do. As an
2037 // example, it may not be safe to clear the pasteboard.
2038 // http://crbug.com/38665
2039}
2040
2041- (void)nodeAdded:(BookmarkModel*)model
2042 parent:(const BookmarkNode*)newParent index:(int)newIndex {
2043 // If a context menu is open, close it.
2044 [self cancelMenuTracking];
2045
2046 const BookmarkNode* newNode = newParent->GetChild(newIndex);
2047 id<BookmarkButtonControllerProtocol> newController =
2048 [self controllerForNode:newParent];
2049 [newController addButtonForNode:newNode atIndex:newIndex];
2050 // If we go from 0 --> 1 bookmarks we may need to hide the
2051 // "bookmarks go here" text container.
2052 [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()];
2053 // Cope with chevron or "Other Bookmarks" buttons possibly changing state.
2054 [self reconfigureBookmarkBar];
2055}
2056
2057// TODO(jrg): for now this is brute force.
2058- (void)nodeChanged:(BookmarkModel*)model
2059 node:(const BookmarkNode*)node {
2060 [self loaded:model];
2061}
2062
2063- (void)nodeMoved:(BookmarkModel*)model
2064 oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex
2065 newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex {
2066 const BookmarkNode* movedNode = newParent->GetChild(newIndex);
2067 id<BookmarkButtonControllerProtocol> oldController =
2068 [self controllerForNode:oldParent];
2069 id<BookmarkButtonControllerProtocol> newController =
2070 [self controllerForNode:newParent];
2071 if (newController == oldController) {
2072 [oldController moveButtonFromIndex:oldIndex toIndex:newIndex];
2073 } else {
2074 [oldController removeButton:oldIndex animate:NO];
2075 [newController addButtonForNode:movedNode atIndex:newIndex];
2076 }
2077 // If the bar is one of the parents we may need to update the visibility
2078 // of the "bookmarks go here" presentation.
2079 [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()];
2080 // Cope with chevron or "Other Bookmarks" buttons possibly changing state.
2081 [self reconfigureBookmarkBar];
2082}
2083
2084- (void)nodeRemoved:(BookmarkModel*)model
2085 parent:(const BookmarkNode*)oldParent index:(int)index {
2086 // If a context menu is open, close it.
2087 [self cancelMenuTracking];
2088
2089 // Locate the parent node. The parent may not be showing, in which case
2090 // we do nothing.
2091 id<BookmarkButtonControllerProtocol> parentController =
2092 [self controllerForNode:oldParent];
2093 [parentController removeButton:index animate:YES];
2094 // If we go from 1 --> 0 bookmarks we may need to show the
2095 // "bookmarks go here" text container.
2096 [self showOrHideNoItemContainerForNode:model->bookmark_bar_node()];
2097 // If we deleted the only item on the "off the side" menu we no
2098 // longer need to show it.
2099 [self reconfigureBookmarkBar];
2100}
2101
2102// TODO(jrg): linear searching is bad.
2103// Need a BookmarkNode-->NSCell mapping.
2104//
2105// TODO(jrg): if the bookmark bar is open on launch, we see the
2106// buttons all placed, then "scooted over" as the favicons load. If
2107// this looks bad I may need to change widthForBookmarkButtonCell to
2108// add space for an image even if not there on the assumption that
2109// favicons will eventually load.
2110- (void)nodeFaviconLoaded:(BookmarkModel*)model
2111 node:(const BookmarkNode*)node {
2112 for (BookmarkButton* button in buttons_.get()) {
2113 const BookmarkNode* cellnode = [button bookmarkNode];
2114 if (cellnode == node) {
2115 [[button cell] setBookmarkCellText:[button title]
2116 image:[self faviconForNode:node]];
2117 // Adding an image means we might need more room for the
2118 // bookmark. Test for it by growing the button (if needed)
2119 // and shifting everything else over.
2120 [self checkForBookmarkButtonGrowth:button];
2121 return;
2122 }
2123 }
2124
2125 if (folderController_)
2126 [folderController_ faviconLoadedForNode:node];
2127}
2128
2129// TODO(jrg): for now this is brute force.
2130- (void)nodeChildrenReordered:(BookmarkModel*)model
2131 node:(const BookmarkNode*)node {
2132 [self loaded:model];
2133}
2134
2135#pragma mark BookmarkBarState Protocol
2136
2137// (BookmarkBarState protocol)
2138- (BOOL)isVisible {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002139 return barIsEnabled_ && (currentState_ == BookmarkBar::SHOW ||
2140 currentState_ == BookmarkBar::DETACHED ||
2141 lastState_ == BookmarkBar::SHOW ||
2142 lastState_ == BookmarkBar::DETACHED);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002143}
2144
2145// (BookmarkBarState protocol)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002146- (BOOL)isInState:(BookmarkBar::State)state {
2147 return currentState_ == state && ![self isAnimationRunning];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002148}
2149
2150// (BookmarkBarState protocol)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002151- (BOOL)isAnimatingToState:(BookmarkBar::State)state {
2152 return currentState_ == state && [self isAnimationRunning];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002153}
2154
2155// (BookmarkBarState protocol)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002156- (BOOL)isAnimatingFromState:(BookmarkBar::State)state {
2157 return lastState_ == state && [self isAnimationRunning];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002158}
2159
2160// (BookmarkBarState protocol)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002161- (BOOL)isAnimatingFromState:(BookmarkBar::State)fromState
2162 toState:(BookmarkBar::State)toState {
2163 return lastState_ == fromState &&
2164 currentState_ == toState &&
2165 [self isAnimationRunning];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002166}
2167
2168// (BookmarkBarState protocol)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002169- (BOOL)isAnimatingBetweenState:(BookmarkBar::State)fromState
2170 andState:(BookmarkBar::State)toState {
2171 return [self isAnimatingFromState:fromState toState:toState] ||
2172 [self isAnimatingFromState:toState toState:fromState];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002173}
2174
2175// (BookmarkBarState protocol)
2176- (CGFloat)detachedMorphProgress {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002177 if ([self isInState:BookmarkBar::DETACHED]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002178 return 1;
2179 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002180 if ([self isAnimatingToState:BookmarkBar::DETACHED]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002181 return static_cast<CGFloat>(
2182 [[self animatableView] currentAnimationProgress]);
2183 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002184 if ([self isAnimatingFromState:BookmarkBar::DETACHED]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002185 return static_cast<CGFloat>(
2186 1 - [[self animatableView] currentAnimationProgress]);
2187 }
2188 return 0;
2189}
2190
2191#pragma mark BookmarkBarToolbarViewController Protocol
2192
2193- (int)currentTabContentsHeight {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002194 BrowserWindowController* browserController =
2195 [BrowserWindowController browserWindowControllerForView:[self view]];
2196 return NSHeight([[browserController tabContentArea] frame]);
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002197}
2198
Torne (Richard Coles)b2df76e2013-05-13 16:52:09 +01002199- (ThemeService*)themeService {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002200 return ThemeServiceFactory::GetForProfile(browser_->profile());
2201}
2202
2203#pragma mark BookmarkButtonDelegate Protocol
2204
2205- (void)fillPasteboard:(NSPasteboard*)pboard
2206 forDragOfButton:(BookmarkButton*)button {
2207 [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
2208}
2209
2210// BookmarkButtonDelegate protocol implementation. When menus are
2211// "active" (e.g. you clicked to open one), moving the mouse over
2212// another folder button should close the 1st and open the 2nd (like
2213// real menus). We detect and act here.
2214- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
2215 DCHECK([sender isKindOfClass:[BookmarkButton class]]);
2216
2217 // If folder menus are not being shown, do nothing. This is different from
2218 // BookmarkBarFolderController's implementation because the bar should NOT
2219 // automatically open folder menus when the mouse passes over a folder
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002220 // button while the BookmarkBarFolderController DOES automatically open
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002221 // a subfolder menu.
2222 if (!showFolderMenus_)
2223 return;
2224
2225 // From here down: same logic as BookmarkBarFolderController.
2226 // TODO(jrg): find a way to share these 4 non-comment lines?
2227 // http://crbug.com/35966
2228 // If already opened, then we exited but re-entered the button, so do nothing.
2229 if ([folderController_ parentButton] == sender)
2230 return;
2231 // Else open a new one if it makes sense to do so.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002232 const BookmarkNode* node = [sender bookmarkNode];
2233 if (node && node->is_folder()) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002234 // Update |hoverButton_| so that it corresponds to the open folder.
2235 hoverButton_.reset([sender retain]);
2236 [folderTarget_ openBookmarkFolderFromButton:sender];
2237 } else {
2238 // We're over a non-folder bookmark so close any old folders.
2239 [folderController_ close];
2240 folderController_ = nil;
2241 }
2242}
2243
2244// BookmarkButtonDelegate protocol implementation.
2245- (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
2246 // Don't care; do nothing.
2247 // This is different behavior that the folder menus.
2248}
2249
2250- (NSWindow*)browserWindow {
2251 return [[self view] window];
2252}
2253
2254- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
2255 return [self canEditBookmarks] &&
2256 [self canEditBookmark:[button bookmarkNode]];
2257}
2258
2259- (void)didDragBookmarkToTrash:(BookmarkButton*)button {
2260 if ([self canDragBookmarkButtonToTrash:button]) {
2261 const BookmarkNode* node = [button bookmarkNode];
2262 if (node) {
2263 const BookmarkNode* parent = node->parent();
2264 bookmarkModel_->Remove(parent,
2265 parent->GetIndexOf(node));
2266 }
2267 }
2268}
2269
2270- (void)bookmarkDragDidEnd:(BookmarkButton*)button
2271 operation:(NSDragOperation)operation {
2272 [button setHidden:NO];
2273 [self resetAllButtonPositionsWithAnimation:YES];
2274}
2275
2276
2277#pragma mark BookmarkButtonControllerProtocol
2278
2279// Close all bookmark folders. "Folder" here is the fake menu for
2280// bookmark folders, not a button context menu.
2281- (void)closeAllBookmarkFolders {
2282 [self watchForExitEvent:NO];
2283 [folderController_ close];
2284 folderController_ = nil;
2285}
2286
2287- (void)closeBookmarkFolder:(id)sender {
2288 // We're the top level, so close one means close them all.
2289 [self closeAllBookmarkFolders];
2290}
2291
2292- (BookmarkModel*)bookmarkModel {
2293 return bookmarkModel_;
2294}
2295
2296- (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
2297 return [self canEditBookmarks];
2298}
2299
2300// TODO(jrg): much of this logic is duped with
2301// [BookmarkBarFolderController draggingEntered:] except when noted.
2302// http://crbug.com/35966
2303- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
2304 NSPoint point = [info draggingLocation];
2305 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
2306
2307 // Don't allow drops that would result in cycles.
2308 if (button) {
2309 NSData* data = [[info draggingPasteboard]
2310 dataForType:kBookmarkButtonDragType];
2311 if (data && [info draggingSource]) {
2312 BookmarkButton* sourceButton = nil;
2313 [data getBytes:&sourceButton length:sizeof(sourceButton)];
2314 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
2315 const BookmarkNode* destNode = [button bookmarkNode];
2316 if (destNode->HasAncestor(sourceNode))
2317 button = nil;
2318 }
2319 }
2320
2321 if ([button isFolder]) {
2322 if (hoverButton_ == button) {
2323 return NSDragOperationMove; // already open or timed to open
2324 }
2325 if (hoverButton_) {
2326 // Oops, another one triggered or open.
2327 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_
2328 target]];
2329 // Unlike BookmarkBarFolderController, we do not delay the close
2330 // of the previous one. Given the lack of diagonal movement,
2331 // there is no need, and it feels awkward to do so. See
2332 // comments about kDragHoverCloseDelay in
2333 // bookmark_bar_folder_controller.mm for more details.
2334 [[hoverButton_ target] closeBookmarkFolder:hoverButton_];
2335 hoverButton_.reset();
2336 }
2337 hoverButton_.reset([button retain]);
2338 DCHECK([[hoverButton_ target]
2339 respondsToSelector:@selector(openBookmarkFolderFromButton:)]);
2340 [[hoverButton_ target]
2341 performSelector:@selector(openBookmarkFolderFromButton:)
2342 withObject:hoverButton_
2343 afterDelay:bookmarks::kDragHoverOpenDelay
2344 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
2345 }
2346 if (!button) {
2347 if (hoverButton_) {
2348 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]];
2349 [[hoverButton_ target] closeBookmarkFolder:hoverButton_];
2350 hoverButton_.reset();
2351 }
2352 }
2353
2354 // Thrown away but kept to be consistent with the draggingEntered: interface.
2355 return NSDragOperationMove;
2356}
2357
2358- (void)draggingExited:(id<NSDraggingInfo>)info {
2359 // Only close the folder menu if the user dragged up past the BMB. If the user
2360 // dragged to below the BMB, they might be trying to drop a link into the open
2361 // folder menu.
2362 // TODO(asvitkine): Need a way to close the menu if the user dragged below but
2363 // not into the menu.
2364 NSRect bounds = [[self view] bounds];
2365 NSPoint origin = [[self view] convertPoint:bounds.origin toView:nil];
2366 if ([info draggingLocation].y > origin.y + bounds.size.height)
2367 [self closeFolderAndStopTrackingMenus];
2368
2369 // NOT the same as a cancel --> we may have moved the mouse into the submenu.
2370 if (hoverButton_) {
2371 [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]];
2372 hoverButton_.reset();
2373 }
2374}
2375
2376- (BOOL)dragShouldLockBarVisibility {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002377 return ![self isInState:BookmarkBar::DETACHED] &&
2378 ![self isAnimatingToState:BookmarkBar::DETACHED];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002379}
2380
2381// TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController.
2382// http://crbug.com/35966
2383- (BOOL)dragButton:(BookmarkButton*)sourceButton
2384 to:(NSPoint)point
2385 copy:(BOOL)copy {
2386 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
2387 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
2388 return [self dragBookmark:sourceNode to:point copy:copy];
2389}
2390
2391- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
2392 BOOL dragged = NO;
2393 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
2394 if (nodes.size()) {
2395 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
2396 NSPoint dropPoint = [info draggingLocation];
2397 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
2398 it != nodes.end(); ++it) {
2399 const BookmarkNode* sourceNode = *it;
2400 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
2401 }
2402 }
2403 return dragged;
2404}
2405
2406- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
2407 std::vector<const BookmarkNode*> dragDataNodes;
2408 BookmarkNodeData dragData;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002409 if (dragData.ReadFromDragClipboard()) {
2410 std::vector<const BookmarkNode*> nodes(
2411 dragData.GetNodes(browser_->profile()));
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002412 dragDataNodes.assign(nodes.begin(), nodes.end());
2413 }
2414 return dragDataNodes;
2415}
2416
2417// Return YES if we should show the drop indicator, else NO.
2418- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
2419 return ![self buttonForDroppingOnAtPoint:point];
2420}
2421
2422// Return the x position for a drop indicator.
2423- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
2424 CGFloat x = 0;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002425 CGFloat halfHorizontalPadding = 0.5 * bookmarks::kBookmarkHorizontalPadding;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002426 int destIndex = [self indexForDragToPoint:point];
2427 int numButtons = displayedButtonCount_;
2428
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002429 CGFloat leftmostX;
2430 if ([appsPageShortcutButton_ isHidden])
2431 leftmostX = bookmarks::kBookmarkLeftMargin - halfHorizontalPadding;
2432 else
2433 leftmostX = NSMaxX([appsPageShortcutButton_ frame]) + halfHorizontalPadding;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002434
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002435 // If it's a drop strictly between existing buttons ...
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002436 if (destIndex == 0) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002437 x = leftmostX;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002438 } else if (destIndex > 0 && destIndex < numButtons) {
2439 // ... put the indicator right between the buttons.
2440 BookmarkButton* button =
2441 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex-1)];
2442 DCHECK(button);
2443 NSRect buttonFrame = [button frame];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002444 x = NSMaxX(buttonFrame) + halfHorizontalPadding;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002445
2446 // If it's a drop at the end (past the last button, if there are any) ...
2447 } else if (destIndex == numButtons) {
2448 // and if it's past the last button ...
2449 if (numButtons > 0) {
2450 // ... find the last button, and put the indicator to its right.
2451 BookmarkButton* button =
2452 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
2453 DCHECK(button);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002454 x = NSMaxX([button frame]) + halfHorizontalPadding;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002455
2456 // Otherwise, put it right at the beginning.
2457 } else {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002458 x = leftmostX;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002459 }
2460 } else {
2461 NOTREACHED();
2462 }
2463
2464 return x;
2465}
2466
2467- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
2468 // If the bookmarkbar is not in detached mode, lock bar visibility, forcing
2469 // the overlay to stay open when in fullscreen mode.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002470 if (![self isInState:BookmarkBar::DETACHED] &&
2471 ![self isAnimatingToState:BookmarkBar::DETACHED]) {
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002472 BrowserWindowController* browserController =
2473 [BrowserWindowController browserWindowControllerForView:[self view]];
2474 [browserController lockBarVisibilityForOwner:child
2475 withAnimation:NO
2476 delay:NO];
2477 }
2478}
2479
2480- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
2481 // Release bar visibility, allowing the overlay to close if in fullscreen
2482 // mode.
2483 BrowserWindowController* browserController =
2484 [BrowserWindowController browserWindowControllerForView:[self view]];
2485 [browserController releaseBarVisibilityForOwner:child
2486 withAnimation:NO
2487 delay:NO];
2488}
2489
2490// Add a new folder controller as triggered by the given folder button.
2491- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
2492
2493 // If doing a close/open, make sure the fullscreen chrome doesn't
2494 // have a chance to begin animating away in the middle of things.
2495 BrowserWindowController* browserController =
2496 [BrowserWindowController browserWindowControllerForView:[self view]];
2497 // Confirm we're not re-locking with ourself as an owner before locking.
2498 DCHECK([browserController isBarVisibilityLockedForOwner:self] == NO);
2499 [browserController lockBarVisibilityForOwner:self
2500 withAnimation:NO
2501 delay:NO];
2502
2503 if (folderController_)
2504 [self closeAllBookmarkFolders];
2505
2506 // Folder controller, like many window controllers, owns itself.
2507 folderController_ =
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01002508 [[BookmarkBarFolderController alloc]
2509 initWithParentButton:parentButton
2510 parentController:nil
2511 barController:self
2512 profile:browser_->profile()];
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002513 [folderController_ showWindow:self];
2514
2515 // Only BookmarkBarController has this; the
2516 // BookmarkBarFolderController does not.
2517 [self watchForExitEvent:YES];
2518
2519 // No longer need to hold the lock; the folderController_ now owns it.
2520 [browserController releaseBarVisibilityForOwner:self
2521 withAnimation:NO
2522 delay:NO];
2523}
2524
2525- (void)openAll:(const BookmarkNode*)node
2526 disposition:(WindowOpenDisposition)disposition {
2527 [self closeFolderAndStopTrackingMenus];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002528 chrome::OpenAll([[self view] window], browser_, node, disposition,
2529 browser_->profile());
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002530}
2531
2532- (void)addButtonForNode:(const BookmarkNode*)node
2533 atIndex:(NSInteger)buttonIndex {
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002534 int newOffset =
2535 bookmarks::kBookmarkLeftMargin - bookmarks::kBookmarkHorizontalPadding;
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002536 if (buttonIndex == -1)
2537 buttonIndex = [buttons_ count]; // New button goes at the end.
2538 if (buttonIndex <= (NSInteger)[buttons_ count]) {
2539 if (buttonIndex) {
2540 BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1];
2541 NSRect targetFrame = [targetButton frame];
2542 newOffset = targetFrame.origin.x + NSWidth(targetFrame) +
2543 bookmarks::kBookmarkHorizontalPadding;
2544 }
2545 BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset];
2546 ++displayedButtonCount_;
2547 [buttons_ insertObject:newButton atIndex:buttonIndex];
2548 [buttonView_ addSubview:newButton];
2549 [self resetAllButtonPositionsWithAnimation:NO];
2550 // See if any buttons need to be pushed off to or brought in from the side.
2551 [self reconfigureBookmarkBar];
2552 } else {
2553 // A button from somewhere else (not the bar) is being moved to the
2554 // off-the-side so insure it gets redrawn if its showing.
2555 [self reconfigureBookmarkBar];
2556 [folderController_ reconfigureMenu];
2557 }
2558}
2559
2560// TODO(mrossetti): Duplicate code with BookmarkBarFolderController.
2561// http://crbug.com/35966
2562- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
2563 DCHECK([urls count] == [titles count]);
2564 BOOL nodesWereAdded = NO;
2565 // Figure out where these new bookmarks nodes are to be added.
2566 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
2567 const BookmarkNode* destParent = NULL;
2568 int destIndex = 0;
2569 if ([button isFolder]) {
2570 destParent = [button bookmarkNode];
2571 // Drop it at the end.
2572 destIndex = [button bookmarkNode]->child_count();
2573 } else {
2574 // Else we're dropping somewhere on the bar, so find the right spot.
2575 destParent = bookmarkModel_->bookmark_bar_node();
2576 destIndex = [self indexForDragToPoint:point];
2577 }
2578
2579 // Don't add the bookmarks if the destination index shows an error.
2580 if (destIndex >= 0) {
2581 // Create and add the new bookmark nodes.
2582 size_t urlCount = [urls count];
2583 for (size_t i = 0; i < urlCount; ++i) {
2584 GURL gurl;
2585 const char* string = [[urls objectAtIndex:i] UTF8String];
2586 if (string)
2587 gurl = GURL(string);
2588 // We only expect to receive valid URLs.
2589 DCHECK(gurl.is_valid());
2590 if (gurl.is_valid()) {
2591 bookmarkModel_->AddURL(destParent,
2592 destIndex++,
2593 base::SysNSStringToUTF16(
2594 [titles objectAtIndex:i]),
2595 gurl);
2596 nodesWereAdded = YES;
2597 }
2598 }
2599 }
2600 return nodesWereAdded;
2601}
2602
2603- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
2604 if (fromIndex != toIndex) {
2605 NSInteger buttonCount = (NSInteger)[buttons_ count];
2606 if (toIndex == -1)
2607 toIndex = buttonCount;
2608 // See if we have a simple move within the bar, which will be the case if
2609 // both button indexes are in the visible space.
2610 if (fromIndex < buttonCount && toIndex < buttonCount) {
2611 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
2612 [buttons_ removeObjectAtIndex:fromIndex];
2613 [buttons_ insertObject:movedButton atIndex:toIndex];
2614 [movedButton setHidden:NO];
2615 [self resetAllButtonPositionsWithAnimation:NO];
2616 } else if (fromIndex < buttonCount) {
2617 // A button is being removed from the bar and added to off-the-side.
2618 // By now the node has already been inserted into the model so the
2619 // button to be added is represented by |toIndex|. Things get
2620 // complicated because the off-the-side is showing and must be redrawn
2621 // while possibly re-laying out the bookmark bar.
2622 [self removeButton:fromIndex animate:NO];
2623 [self reconfigureBookmarkBar];
2624 [folderController_ reconfigureMenu];
2625 } else if (toIndex < buttonCount) {
2626 // A button is being added to the bar and removed from off-the-side.
2627 // By now the node has already been inserted into the model so the
2628 // button to be added is represented by |toIndex|.
2629 const BookmarkNode* node = bookmarkModel_->bookmark_bar_node();
2630 const BookmarkNode* movedNode = node->GetChild(toIndex);
2631 DCHECK(movedNode);
2632 [self addButtonForNode:movedNode atIndex:toIndex];
2633 [self reconfigureBookmarkBar];
2634 } else {
2635 // A button is being moved within the off-the-side.
2636 fromIndex -= buttonCount;
2637 toIndex -= buttonCount;
2638 [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex];
2639 }
2640 }
2641}
2642
2643- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
2644 if (buttonIndex < (NSInteger)[buttons_ count]) {
2645 // The button being removed is showing in the bar.
2646 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
2647 if (oldButton == [folderController_ parentButton]) {
2648 // If we are deleting a button whose folder is currently open, close it!
2649 [self closeAllBookmarkFolders];
2650 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00002651 if (animate && innerContentAnimationsEnabled_ && [self isVisible] &&
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002652 [[self browserWindow] isMainWindow]) {
2653 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
2654 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
2655 NSZeroSize, nil, nil, nil);
2656 }
2657 [oldButton setDelegate:nil];
2658 [oldButton removeFromSuperview];
2659 [buttons_ removeObjectAtIndex:buttonIndex];
2660 --displayedButtonCount_;
2661 [self resetAllButtonPositionsWithAnimation:YES];
2662 [self reconfigureBookmarkBar];
2663 } else if (folderController_ &&
2664 [folderController_ parentButton] == offTheSideButton_) {
2665 // The button being removed is in the OTS (off-the-side) and the OTS
2666 // menu is showing so we need to remove the button.
2667 NSInteger index = buttonIndex - displayedButtonCount_;
2668 [folderController_ removeButton:index animate:YES];
2669 }
2670}
2671
2672- (id<BookmarkButtonControllerProtocol>)controllerForNode:
2673 (const BookmarkNode*)node {
2674 // See if it's in the bar, then if it is in the hierarchy of visible
2675 // folder menus.
2676 if (bookmarkModel_->bookmark_bar_node() == node)
2677 return self;
2678 return [folderController_ controllerForNode:node];
2679}
2680
Torne (Richard Coles)58218062012-11-14 11:43:16 +00002681@end