diff options
Diffstat (limited to 'chrome/browser/cocoa/bookmark_bar_controller.mm')
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_controller.mm | 2403 |
1 files changed, 2403 insertions, 0 deletions
diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm new file mode 100644 index 0000000..f05f9cd --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_controller.mm @@ -0,0 +1,2403 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/cocoa/bookmark_bar_controller.h" +#include "app/l10n_util_mac.h" +#include "app/resource_bundle.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_editor.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/bookmarks/bookmark_utils.h" +#include "chrome/browser/browser.h" +#include "chrome/browser/browser_list.h" +#import "chrome/browser/browser_theme_provider.h" +#import "chrome/browser/cocoa/background_gradient_view.h" +#import "chrome/browser/cocoa/bookmark_bar_bridge.h" +#import "chrome/browser/cocoa/bookmark_bar_constants.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_window.h" +#import "chrome/browser/cocoa/bookmark_bar_toolbar_view.h" +#import "chrome/browser/cocoa/bookmark_bar_view.h" +#import "chrome/browser/cocoa/bookmark_button.h" +#import "chrome/browser/cocoa/bookmark_button_cell.h" +#import "chrome/browser/cocoa/bookmark_editor_controller.h" +#import "chrome/browser/cocoa/bookmark_folder_target.h" +#import "chrome/browser/cocoa/bookmark_menu.h" +#import "chrome/browser/cocoa/bookmark_menu_cocoa_controller.h" +#import "chrome/browser/cocoa/bookmark_name_folder_controller.h" +#import "chrome/browser/cocoa/browser_window_controller.h" +#import "chrome/browser/cocoa/event_utils.h" +#import "chrome/browser/cocoa/fullscreen_controller.h" +#import "chrome/browser/cocoa/import_settings_dialog.h" +#import "chrome/browser/cocoa/menu_button.h" +#import "chrome/browser/cocoa/themed_window.h" +#import "chrome/browser/cocoa/toolbar_controller.h" +#import "chrome/browser/cocoa/view_id_util.h" +#import "chrome/browser/cocoa/view_resizer.h" +#include "chrome/browser/metrics/user_metrics.h" +#include "chrome/browser/pref_service.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/tab_contents_view.h" +#include "chrome/common/pref_names.h" +#include "grit/app_resources.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "skia/ext/skia_utils_mac.h" + +// Bookmark bar state changing and animations +// +// The bookmark bar has three real states: "showing" (a normal bar attached to +// the toolbar), "hidden", and "detached" (pretending to be part of the web +// content on the NTP). It can, or at least should be able to, animate between +// these states. There are several complications even without animation: +// - The placement of the bookmark bar is done by the BWC, and it needs to know +// the state in order to place the bookmark bar correctly (immediately below +// the toolbar when showing, below the infobar when detached). +// - The "divider" (a black line) needs to be drawn by either the toolbar (when +// the bookmark bar is hidden or detached) or by the bookmark bar (when it is +// showing). It should not be drawn by both. +// - The toolbar needs to vertically "compress" when the bookmark bar is +// showing. This ensures the proper display of both the bookmark bar and the +// toolbar, and gives a padded area around the bookmark bar items for right +// clicks, etc. +// +// Our model is that the BWC controls us and also the toolbar. We try not to +// talk to the browser nor the toolbar directly, instead centralizing control in +// the BWC. The key method by which the BWC controls us is +// |-updateAndShowNormalBar:showDetachedBar:withAnimation:|. This invokes state +// changes, and at appropriate times we request that the BWC do things for us +// via either the resize delegate or our general delegate. If the BWC needs any +// information about what it should do, or tell the toolbar to do, it can then +// query us back (e.g., |-isShownAs...|, |-getDesiredToolbarHeightCompression|, +// |-toolbarDividerOpacity|, etc.). +// +// Animation-related complications: +// - Compression of the toolbar is touchy during animation. It must not be +// compressed while the bookmark bar is animating to/from showing (from/to +// hidden), otherwise it would look like the bookmark bar's contents are +// sliding out of the controls inside the toolbar. As such, we have to make +// sure that the bookmark bar is shown at the right location and at the +// right height (at various points in time). +// - Showing the divider is also complicated during animation between hidden +// and showing. We have to make sure that the toolbar does not show the +// divider despite the fact that it's not compressed. The exception to this +// is at the beginning/end of the animation when the toolbar is still +// uncompressed but the bookmark bar has height 0. If we're not careful, we +// get a flicker at this point. +// - We have to ensure that we do the right thing if we're told to change state +// while we're running an animation. The generic/easy thing to do is to jump +// to the end state of our current animation, and (if the new state change +// again involves an animation) begin the new animation. We can do better +// than that, however, and sometimes just change the current animation to go +// to the new end state (e.g., by "reversing" the animation in the showing -> +// hidden -> showing case). We also have to ensure that demands to +// immediately change state are always honoured. +// +// Pointers to animation logic: +// - |-moveToVisualState:withAnimation:| starts animations, deciding which ones +// we know how to handle. +// - |-doBookmarkBarAnimation| has most of the actual logic. +// - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain +// related logic. +// - The BWC's |-layoutSubviews| needs to know how to position things. +// - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and +// |-bookmarkBar:willAnimateFromState:toState:| in order to inform the +// toolbar of required changes. + +namespace { + +// Overlap (in pixels) between the toolbar and the bookmark bar (when showing in +// normal mode). +const CGFloat kBookmarkBarOverlap = 3.0; + +// Duration of the bookmark bar animations. +const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; + +} // namespace + +@interface BookmarkBarController(Private) + +// Determines the appropriate state for the given situation. ++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar; + +// Moves to the given next state (from the current state), possibly animating. +// If |animate| is NO, it will stop any running animation and jump to the given +// state. If YES, it may either (depending on implementation) jump to the end of +// the current animation and begin the next one, or stop the current animation +// mid-flight and animate to the next state. +- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState + withAnimation:(BOOL)animate; + +// Return the backdrop to the bookmark bar as various types. +- (BackgroundGradientView*)backgroundGradientView; +- (AnimatableView*)animatableView; + +// Create buttons for all items in the given bookmark node tree. +// Modifies self->buttons_. Do not add more buttons than will fit on the view. +- (void)addNodesToButtonList:(const BookmarkNode*)node; + +// Create an autoreleased button appropriate for insertion into the bookmark +// bar. Update |xOffset| with the offset appropriate for the subsequent button. +- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node + xOffset:(int*)xOffset; + +// Puts stuff into the final visual state without animating, stopping a running +// animation if necessary. +- (void)finalizeVisualState; + +// Stops any current animation in its tracks (midway). +- (void)stopCurrentAnimation; + +// Show/hide the bookmark bar. +// if |animate| is YES, the changes are made using the animator; otherwise they +// are made immediately. +- (void)showBookmarkBarWithAnimation:(BOOL)animate; + +// Handles animating the resize of the content view. Returns YES if it handled +// the animation, NO if not (and hence it should be done instantly). +- (BOOL)doBookmarkBarAnimation; + +// |point| is in the base coordinate system of the destination window; +// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be +// made and inserted into the new location while leaving the bookmark in +// the old location, otherwise move the bookmark by removing from its old +// location and inserting into the new location. +- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode + to:(NSPoint)point + copy:(BOOL)copy; + +// Returns the index in the model for a drag to the location given by +// |point|. This is determined by finding the first button before the center +// of which |point| falls, scanning left to right. Note that, currently, only +// the x-coordinate of |point| is considered. Though not currently implemented, +// we may check for errors, in which case this would return negative value; +// callers should check for this. +- (int)indexForDragToPoint:(NSPoint)point; + +// Add or remove buttons to/from the bar until it is filled but not overflowed. +- (void)redistributeButtonsOnBarAsNeeded; + +// Determine the nature of the bookmark bar contents based on the number of +// buttons showing. If too many then show the off-the-side list, if none +// then show the no items label. +- (void)reconfigureBookmarkBar; + +- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu; +- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu; +- (void)tagEmptyMenu:(NSMenu*)menu; +- (void)clearMenuTagMap; +- (int)preferredHeight; +- (void)addNonBookmarkButtonsToView; +- (void)addButtonsToView; +- (void)centerNoItemsLabel; +- (void)setNodeForBarMenu; + +- (void)watchForExitEvent:(BOOL)watch; + +@end + +@implementation BookmarkBarController + +@synthesize visualState = visualState_; +@synthesize lastVisualState = lastVisualState_; +@synthesize delegate = delegate_; + +- (id)initWithBrowser:(Browser*)browser + initialWidth:(float)initialWidth + delegate:(id<BookmarkBarControllerDelegate>)delegate + resizeDelegate:(id<ViewResizer>)resizeDelegate { + if ((self = [super initWithNibName:@"BookmarkBar" + bundle:mac_util::MainAppBundle()])) { + // Initialize to an invalid state. + visualState_ = bookmarks::kInvalidState; + lastVisualState_ = bookmarks::kInvalidState; + + browser_ = browser; + initialWidth_ = initialWidth; + bookmarkModel_ = browser_->profile()->GetBookmarkModel(); + buttons_.reset([[NSMutableArray alloc] init]); + delegate_ = delegate; + resizeDelegate_ = resizeDelegate; + folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]); + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + folderImage_.reset([rb.GetNSImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]); + defaultImage_.reset([rb.GetNSImageNamed(IDR_DEFAULT_FAVICON) retain]); + + // Register for theme changes. + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter addObserver:self + selector:@selector(themeDidChangeNotification:) + name:kBrowserThemeDidChangeNotification + object:nil]; + + // This call triggers an awakeFromNib, which builds the bar, which + // might uses folderImage_. So make sure it happens after + // folderImage_ is loaded. + [[self animatableView] setResizeDelegate:resizeDelegate]; + } + return self; +} + +- (void)dealloc { + // We better stop any in-flight animation if we're being killed. + [[self animatableView] stopAnimation]; + + // Remove our view from its superview so it doesn't attempt to reference + // it when the controller is gone. + //TODO(dmaclach): Remove -- http://crbug.com/25845 + [[self view] removeFromSuperview]; + + // Be sure there is no dangling pointer. + if ([[self view] respondsToSelector:@selector(setController:)]) + [[self view] performSelector:@selector(setController:) withObject:nil]; + + // For safety, make sure the buttons can no longer call us. + for (BookmarkButton* button in buttons_.get()) { + [button setDelegate:nil]; + [button setTarget:nil]; + [button setAction:nil]; + } + + bridge_.reset(NULL); + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self watchForExitEvent:NO]; + [super dealloc]; +} + +- (void)awakeFromNib { + // We default to NOT open, which means height=0. + DCHECK([[self view] isHidden]); // Hidden so it's OK to change. + + // Set our initial height to zero, since that is what the superview + // expects. We will resize ourselves open later if needed. + [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)]; + + // Complete init of the "off the side" button, as much as we can. + [offTheSideButton_ setDraggable:NO]; + + // We are enabled by default. + barIsEnabled_ = YES; + + // Remember the original sizes of the 'no items' and 'import bookmarks' + // fields to aid in resizing when the window frame changes. + originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame]; + originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame]; + + // To make life happier when the bookmark bar is floating, the chevron is a + // child of the button view. + [offTheSideButton_ removeFromSuperview]; + [buttonView_ addSubview:offTheSideButton_]; + + // Copy the bar menu so we know if it's from the bar or a folder. + // Then we set its represented item to be the bookmark bar. + buttonFolderContextMenu_.reset([[[self view] menu] copy]); + + // When resized we may need to add new buttons, or remove them (if + // no longer visible), or add/remove the "off the side" menu. + [[self view] setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(frameDidChange) + name:NSViewFrameDidChangeNotification + object:[self view]]; + + // Watch for things going to or from fullscreen. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(willEnterOrLeaveFullscreen:) + name:kWillEnterFullscreenNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(willEnterOrLeaveFullscreen:) + name:kWillLeaveFullscreenNotification + object:nil]; + + // Don't pass ourself along (as 'self') until our init is completely + // done. Thus, this call is (almost) last. + bridge_.reset(new BookmarkBarBridge(self, bookmarkModel_)); +} + +// Called by our main view (a BookmarkBarView) when it gets moved to a +// window. We perform operations which need to know the relevant +// window (e.g. watch for a window close) so they can't be performed +// earlier (such as in awakeFromNib). +- (void)viewDidMoveToWindow { + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + + // Remove any existing notifications before registering for new ones. + [defaultCenter removeObserver:self + name:NSWindowWillCloseNotification + object:nil]; + [defaultCenter removeObserver:self + name:NSWindowDidResignKeyNotification + object:nil]; + + [defaultCenter addObserver:self + selector:@selector(parentWindowWillClose:) + name:NSWindowWillCloseNotification + object:[[self view] window]]; + [defaultCenter addObserver:self + selector:@selector(parentWindowDidResignKey:) + name:NSWindowDidResignKeyNotification + object:[[self view] window]]; +} + +// When going fullscreen we can run into trouble. Our view is removed +// from the non-fullscreen window before the non-fullscreen window +// loses key, so our parentDidResignKey: callback never gets called. +// In addition, a bookmark folder controller needs to be autoreleased +// (in case it's in the event chain when closed), but the release +// implicitly needs to happen while it's connected to the original +// (non-fullscreen) window to "unlock bar visibility". Such a +// contract isn't honored when going fullscreen with the menu option +// (not with the keyboard shortcut). We fake it as best we can here. +// We have a similar problem leaving fullscreen. +- (void)willEnterOrLeaveFullscreen:(NSNotification*)notification { + if (folderController_) { + [self childFolderWillClose:folderController_]; + [self closeFolderAndStopTrackingMenus]; + } +} + +// NSNotificationCenter callback. +- (void)parentWindowWillClose:(NSNotification*)notification { + [self closeFolderAndStopTrackingMenus]; +} + +// NSNotificationCenter callback. +- (void)parentWindowDidResignKey:(NSNotification*)notification { + [self closeFolderAndStopTrackingMenus]; +} + +// Change the layout of the bookmark bar's subviews in response to a visibility +// change (e.g., show or hide the bar) or style change (attached or floating). +- (void)layoutSubviews { + NSRect frame = [[self view] frame]; + NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame)); + + // The state of our morph (if any); 1 is total bubble, 0 is the regular bar. + CGFloat morph = [self detachedMorphProgress]; + + // Add padding to the detached bookmark bar. + buttonViewFrame = NSInsetRect(buttonViewFrame, + morph * bookmarks::kNTPBookmarkBarPadding, + morph * bookmarks::kNTPBookmarkBarPadding); + + [buttonView_ setFrame:buttonViewFrame]; +} + +// We don't change a preference; we only change visibility. Preference changing +// (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We +// simply update based on what we're told. +- (void)updateVisibility { + [self showBookmarkBarWithAnimation:NO]; +} + +- (void)setBookmarkBarEnabled:(BOOL)enabled { + if (enabled != barIsEnabled_) { + barIsEnabled_ = enabled; + [self updateVisibility]; + } +} + +- (CGFloat)getDesiredToolbarHeightCompression { + // Some special cases.... + if (!barIsEnabled_) + return 0; + + if ([self isAnimationRunning]) { + // No toolbar compression when animating between hidden and showing, nor + // between showing and detached. + if ([self isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState] || + [self isAnimatingBetweenState:bookmarks::kShowingState + andState:bookmarks::kDetachedState]) + return 0; + + // If we ever need any other animation cases, code would go here. + } + + return [self isInState:bookmarks::kShowingState] ? kBookmarkBarOverlap : 0; +} + +- (CGFloat)toolbarDividerOpacity { + // Some special cases.... + if ([self isAnimationRunning]) { + // In general, the toolbar shouldn't show a divider while we're animating + // between showing and hidden. The exception is when our height is < 1, in + // which case we can't draw it. It's all-or-nothing (no partial opacity). + if ([self isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState]) + return (NSHeight([[self view] frame]) < 1) ? 1 : 0; + + // The toolbar should show the divider when animating between showing and + // detached (but opacity will vary). + if ([self isAnimatingBetweenState:bookmarks::kShowingState + andState:bookmarks::kDetachedState]) + return static_cast<CGFloat>([self detachedMorphProgress]); + + // If we ever need any other animation cases, code would go here. + } + + // In general, only show the divider when it's in the normal showing state. + return [self isInState:bookmarks::kShowingState] ? 0 : 1; +} + +- (NSImage*)favIconForNode:(const BookmarkNode*)node { + if (!node) + return defaultImage_; + + if (node->is_folder()) + return folderImage_; + + const SkBitmap& favIcon = bookmarkModel_->GetFavIcon(node); + if (!favIcon.isNull()) + return gfx::SkBitmapToNSImage(favIcon); + + return defaultImage_; +} + +- (void)closeFolderAndStopTrackingMenus { + showFolderMenus_ = NO; + [self closeAllBookmarkFolders]; +} + +#pragma mark Actions + +- (IBAction)openBookmark:(id)sender { + [self closeFolderAndStopTrackingMenus]; + DCHECK([sender respondsToSelector:@selector(bookmarkNode)]); + const BookmarkNode* node = [sender bookmarkNode]; + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); + [self openURL:node->GetURL() disposition:disposition]; +} + +// Redirect to our logic shared with BookmarkBarFolderController. +- (IBAction)openBookmarkFolderFromButton:(id)sender { + DCHECK(sender != offTheSideButton_); + // Toggle presentation of bar folder menus. + showFolderMenus_ = !showFolderMenus_; + [folderTarget_ openBookmarkFolderFromButton:sender]; +} + +// The button that sends this one is special; the "off the side" +// button (chevron) opens like a folder button but isn't exactly a +// parent folder. +- (IBAction)openOffTheSideFolderFromButton:(id)sender { + DCHECK([sender isKindOfClass:[BookmarkButton class]]); + DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]); + [[sender cell] setStartingChildIndex:displayedButtonCount_]; + [folderTarget_ openBookmarkFolderFromButton:sender]; +} + +- (IBAction)openBookmarkInNewForegroundTab:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) + [self openURL:node->GetURL() disposition:NEW_FOREGROUND_TAB]; + [self closeAllBookmarkFolders]; +} + +- (IBAction)openBookmarkInNewWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) + [self openURL:node->GetURL() disposition:NEW_WINDOW]; +} + +- (IBAction)openBookmarkInIncognitoWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) + [self openURL:node->GetURL() disposition:OFF_THE_RECORD]; +} + +- (IBAction)editBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (!node) + return; + + if (node->is_folder()) { + BookmarkNameFolderController* controller = + [[BookmarkNameFolderController alloc] + initWithParentWindow:[[self view] window] + profile:browser_->profile() + node:node]; + [controller runAsModalSheet]; + return; + } + + // There is no real need to jump to a platform-common routine at + // this point (which just jumps back to objc) other than consistency + // across platforms. + // + // TODO(jrg): identify when we NO_TREE. I can see it in the code + // for the other platforms but can't find a way to trigger it in the + // UI. + BookmarkEditor::Show([[self view] window], + browser_->profile(), + node->GetParent(), + BookmarkEditor::EditDetails(node), + BookmarkEditor::SHOW_TREE); +} + +- (IBAction)cutBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, true); + } +} + +- (IBAction)copyBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, false); + } +} + +// Paste the copied node immediately after the node for which the context +// menu has been presented if the node is a non-folder bookmark, otherwise +// past at the end of the folder node. +- (IBAction)pasteBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + int index = -1; + if (node != bookmarkModel_->GetBookmarkBarNode() && !node->is_folder()) { + const BookmarkNode* parent = node->GetParent(); + index = parent->IndexOfChild(node) + 1; + if (index > parent->GetChildCount()) + index = -1; + node = parent; + } + bookmark_utils::PasteFromClipboard(bookmarkModel_, node, index); + } +} + +- (IBAction)deleteBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + bookmarkModel_->Remove(node->GetParent(), + node->GetParent()->IndexOfChild(node)); + } +} + +- (IBAction)openAllBookmarks:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + [self openAll:node disposition:NEW_FOREGROUND_TAB]; + UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarks"), + browser_->profile()); + } +} + +- (IBAction)openAllBookmarksNewWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + [self openAll:node disposition:NEW_WINDOW]; + UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarksNewWindow"), + browser_->profile()); + } +} + +- (IBAction)openAllBookmarksIncognitoWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + [self openAll:node disposition:OFF_THE_RECORD]; + UserMetrics::RecordAction( + UserMetricsAction("OpenAllBookmarksIncognitoWindow"), + browser_->profile()); + } +} + +// May be called from the bar or from a folder button. +// If called from a button, that button becomes the parent. +- (IBAction)addPage:(id)sender { + const BookmarkNode* parent = [self nodeFromMenuItem:sender]; + if (!parent) + parent = bookmarkModel_->GetBookmarkBarNode(); + BookmarkEditor::Show([[self view] window], + browser_->profile(), + parent, + BookmarkEditor::EditDetails(), + BookmarkEditor::SHOW_TREE); +} + +// Might be called from the context menu over the bar OR over a +// button. If called from a button, that button becomes a sibling of +// the new node. If called from the bar, add to the end of the bar. +- (IBAction)addFolder:(id)sender { + const BookmarkNode* senderNode = [self nodeFromMenuItem:sender]; + const BookmarkNode* parent = NULL; + int newIndex = 0; + // If triggered from the bar, folder or "others" folder - add as a child to + // the end. + // If triggered from a bookmark, add as next sibling. + BookmarkNode::Type type = senderNode->type(); + if (type == BookmarkNode::BOOKMARK_BAR || + type == BookmarkNode::OTHER_NODE || + type == BookmarkNode::FOLDER) { + parent = senderNode; + newIndex = parent->GetChildCount(); + } else { + parent = senderNode->GetParent(); + newIndex = parent->IndexOfChild(senderNode) + 1; + } + BookmarkNameFolderController* controller = + [[BookmarkNameFolderController alloc] + initWithParentWindow:[[self view] window] + profile:browser_->profile() + parent:parent + newIndex:newIndex]; + [controller runAsModalSheet]; +} + +- (IBAction)importBookmarks:(id)sender { + [ImportSettingsDialogController showImportSettingsDialogForProfile: + browser_->profile()]; +} + +#pragma mark Private Methods + +// Called after the current theme has changed. +- (void)themeDidChangeNotification:(NSNotification*)aNotification { + ThemeProvider* themeProvider = + static_cast<ThemeProvider*>([[aNotification object] pointerValue]); + [self updateTheme:themeProvider]; +} + +// (Private) Method is the same as [self view], but is provided to be explicit. +- (BackgroundGradientView*)backgroundGradientView { + DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]); + return (BackgroundGradientView*)[self view]; +} + +// (Private) Method is the same as [self view], but is provided to be explicit. +- (AnimatableView*)animatableView { + DCHECK([[self view] isKindOfClass:[AnimatableView class]]); + return (AnimatableView*)[self view]; +} + +// Position the off-the-side chevron to the left of the otherBookmarks button. +- (void)positionOffTheSideButton { + NSRect frame = [offTheSideButton_ frame]; + if (otherBookmarksButton_.get()) { + frame.origin.x = ([otherBookmarksButton_ frame].origin.x - + (frame.size.width + + bookmarks::kBookmarkHorizontalPadding)); + [offTheSideButton_ setFrame:frame]; + } +} + +// Configure the off-the-side button (e.g. specify the node range, +// check if we should enable or disable it, etc). +- (void)configureOffTheSideButtonContentsAndVisibility { + // If deleting a button while off-the-side is open, buttons may be + // promoted from off-the-side to the bar. Accomodate. + if (folderController_ && + ([folderController_ parentButton] == offTheSideButton_)) { + [folderController_ reconfigureMenu]; + } + + [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_]; + [[offTheSideButton_ cell] + setBookmarkNode:bookmarkModel_->GetBookmarkBarNode()]; + int bookmarkChildren = bookmarkModel_->GetBookmarkBarNode()->GetChildCount(); + if (bookmarkChildren > displayedButtonCount_) { + [offTheSideButton_ setHidden:NO]; + } else { + // If we just deleted the last item in an off-the-side menu so the + // button will be going away, make sure the menu goes away. + if (folderController_ && + ([folderController_ parentButton] == offTheSideButton_)) + [self closeAllBookmarkFolders]; + // (And hide the button, too.) + [offTheSideButton_ setHidden:YES]; + } +} + +// Begin (or end) watching for a click outside this window. Unlike +// normal NSWindows, bookmark folder "fake menu" windows do not become +// key or main. Thus, traditional notification (e.g. WillResignKey) +// won't work. Our strategy is to watch (at the app level) for a +// "click outside" these windows to detect when they logically lose +// focus. +- (void)watchForExitEvent:(BOOL)watch { + CrApplication* app = static_cast<CrApplication*>([NSApplication + sharedApplication]); + DCHECK([app isKindOfClass:[CrApplication class]]); + if (watch) { + if (!watchingForExitEvent_) + [app addEventHook:self]; + } else { + if (watchingForExitEvent_) + [app removeEventHook:self]; + } + watchingForExitEvent_ = watch; +} + +// Keep the "no items" label centered in response to a frame size change. +- (void)centerNoItemsLabel { + // Note that this computation is done in the parent's coordinate system, + // which is unflipped. Also, we want the label to be a fixed distance from + // the bottom, so that it slides up properly (on animating to hidden). + // The textfield sits in the itemcontainer, so to center it we maintain + // equal vertical padding on the top and bottom. + int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) - + NSHeight([[buttonView_ noItemContainer] frame])) / 2; + [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)]; +} + +// (Private) +- (void)showBookmarkBarWithAnimation:(BOOL)animate { + if (animate && !ignoreAnimations_) { + // If |-doBookmarkBarAnimation| does the animation, we're done. + if ([self doBookmarkBarAnimation]) + return; + + // Else fall through and do the change instantly. + } + + // Set our height. + [resizeDelegate_ resizeView:[self view] + newHeight:[self preferredHeight]]; + + // Only show the divider if showing the normal bookmark bar. + BOOL showsDivider = [self isInState:bookmarks::kShowingState]; + [[self backgroundGradientView] setShowsDivider:showsDivider]; + + // Make sure we're shown. + [[self view] setHidden:![self isVisible]]; + + // Update everything else. + [self layoutSubviews]; + [self frameDidChange]; +} + +// (Private) +- (BOOL)doBookmarkBarAnimation { + if ([self isAnimatingFromState:bookmarks::kHiddenState + toState:bookmarks::kShowingState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + // Height takes into account the extra height we have since the toolbar + // only compresses when we're done. + [view animateToNewHeight:(bookmarks::kBookmarkBarHeight - + kBookmarkBarOverlap) + duration:kBookmarkBarAnimationDuration]; + } else if ([self isAnimatingFromState:bookmarks::kShowingState + toState:bookmarks::kHiddenState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + [view animateToNewHeight:0 + duration:kBookmarkBarAnimationDuration]; + } else if ([self isAnimatingFromState:bookmarks::kShowingState + toState:bookmarks::kDetachedState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + [view animateToNewHeight:bookmarks::kNTPBookmarkBarHeight + duration:kBookmarkBarAnimationDuration]; + } else if ([self isAnimatingFromState:bookmarks::kDetachedState + toState:bookmarks::kShowingState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + // Height takes into account the extra height we have since the toolbar + // only compresses when we're done. + [view animateToNewHeight:(bookmarks::kBookmarkBarHeight - + kBookmarkBarOverlap) + duration:kBookmarkBarAnimationDuration]; + } else { + // Oops! An animation we don't know how to handle. + return NO; + } + + return YES; +} + +// Enable or disable items. We are the menu delegate for both the bar +// and for bookmark folder buttons. +- (BOOL)validateUserInterfaceItem:(id)item { + // Yes for everything we don't explicitly deny. + if (![item isKindOfClass:[NSMenuItem class]]) + return YES; + + // Yes if we're not a special BookmarkMenu. + if (![[item menu] isKindOfClass:[BookmarkMenu class]]) + return YES; + + // No if we think it's a special BookmarkMenu but have trouble. + const BookmarkNode* node = [self nodeFromMenuItem:item]; + if (!node) + return NO; + + // If this is the bar menu, we only have things to do if there are + // buttons. If this is a folder button menu, we only have things to + // do if the folder has items. + NSMenu* menu = [item menu]; + BOOL thingsToDo = NO; + if (menu == [[self view] menu]) { + thingsToDo = [buttons_ count] ? YES : NO; + } else { + if (node && node->is_folder() && node->GetChildCount()) { + thingsToDo = YES; + } + } + + // Disable openAll* if we have nothing to do. + SEL action = [item action]; + if ((!thingsToDo) && + ((action == @selector(openAllBookmarks:)) || + (action == @selector(openAllBookmarksNewWindow:)) || + (action == @selector(openAllBookmarksIncognitoWindow:)))) { + return NO; + } + + if ((action == @selector(editBookmark:)) || + (action == @selector(deleteBookmark:)) || + (action == @selector(cutBookmark:)) || + (action == @selector(copyBookmark:))) { + // Don't allow edit/delete of the bar node, or of "Other Bookmarks" + if ((node == nil) || + (node == bookmarkModel_->other_node()) || + (node == bookmarkModel_->GetBookmarkBarNode())) { + return NO; + } + } + + if (action == @selector(pasteBookmark:) && + !bookmark_utils::CanPasteFromClipboard(node)) + return NO; + + // If this is an incognito window, don't allow "open in incognito". + if ((action == @selector(openBookmarkInIncognitoWindow:)) || + (action == @selector(openAllBookmarksIncognitoWindow:))) { + if (browser_->profile()->IsOffTheRecord()) { + return NO; + } + } + + // Enabled by default. + return YES; +} + +// Actually open the URL. This is the last chance for a unit test to +// override. +- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition { + browser_->OpenURL(url, GURL(), disposition, PageTransition::AUTO_BOOKMARK); +} + +- (void)clearMenuTagMap { + seedId_ = 0; + menuTagMap_.clear(); +} + +- (int)preferredHeight { + DCHECK(![self isAnimationRunning]); + + if (!barIsEnabled_) + return 0; + + switch (visualState_) { + case bookmarks::kShowingState: + return bookmarks::kBookmarkBarHeight; + case bookmarks::kDetachedState: + return bookmarks::kNTPBookmarkBarHeight; + case bookmarks::kHiddenState: + return 0; + case bookmarks::kInvalidState: + default: + NOTREACHED(); + return 0; + } +} + +// Recursively add the given bookmark node and all its children to +// menu, one menu item per node. +- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu { + NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child]; + NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title + action:nil + keyEquivalent:@""] autorelease]; + [menu addItem:item]; + [item setImage:[self favIconForNode:child]]; + if (child->is_folder()) { + NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease]; + [menu setSubmenu:submenu forItem:item]; + if (child->GetChildCount()) { + [self addFolderNode:child toMenu:submenu]; // potentially recursive + } else { + [self tagEmptyMenu:submenu]; + } + } else { + [item setTarget:self]; + [item setAction:@selector(openBookmarkMenuItem:)]; + [item setTag:[self menuTagFromNodeId:child->id()]]; + // Add a tooltip + std::string url_string = child->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", + base::SysWideToNSString(child->GetTitle()), + url_string.c_str()]; + [item setToolTip:tooltip]; + } +} + +// Empty menus are odd; if empty, add something to look at. +// Matches windows behavior. +- (void)tagEmptyMenu:(NSMenu*)menu { + NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU); + [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title + action:NULL + keyEquivalent:@""] autorelease]]; +} + +// Add the children of the given bookmark node (and their children...) +// to menu, one menu item per node. +- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu { + for (int i = 0; i < node->GetChildCount(); i++) { + const BookmarkNode* child = node->GetChild(i); + [self addNode:child toMenu:menu]; + } +} + +// Return an autoreleased NSMenu that represents the given bookmark +// folder node. +- (NSMenu *)menuForFolderNode:(const BookmarkNode*)node { + if (!node->is_folder()) + return nil; + NSString* title = base::SysWideToNSString(node->GetTitle()); + NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease]; + [self addFolderNode:node toMenu:menu]; + + if (![menu numberOfItems]) { + [self tagEmptyMenu:menu]; + } + return menu; +} + +// Return an appropriate width for the given bookmark button cell. +// The "+2" is needed because, sometimes, Cocoa is off by a tad. +// Example: for a bookmark named "Moma" or "SFGate", it is one pixel +// too small. For "FBL" it is 2 pixels too small. +// For a bookmark named "SFGateFooWoo", it is just fine. +- (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell { + CGFloat desired = [cell cellSize].width + 2; + return std::min(desired, bookmarks::kDefaultBookmarkWidth); +} + +- (IBAction)openBookmarkMenuItem:(id)sender { + int64 tag = [self nodeIdFromMenuTag:[sender tag]]; + const BookmarkNode* node = bookmarkModel_->GetNodeByID(tag); + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); + [self openURL:node->GetURL() disposition:disposition]; +} + +// For the given root node of the bookmark bar, show or hide (as +// appropriate) the "no items" container (text which says "bookmarks +// go here"). +- (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node { + BOOL hideNoItemWarning = node->GetChildCount() > 0; + [[buttonView_ noItemContainer] setHidden:hideNoItemWarning]; +} + +// TODO(jrg): write a "build bar" so there is a nice spot for things +// like the contextual menu which is invoked when not over a +// bookmark. On Safari that menu has a "new folder" option. +- (void)addNodesToButtonList:(const BookmarkNode*)node { + [self showOrHideNoItemContainerForNode:node]; + + CGFloat maxViewX = NSMaxX([[self view] bounds]); + int xOffset = 0; + for (int i = 0; i < node->GetChildCount(); i++) { + const BookmarkNode* child = node->GetChild(i); + BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; + if (NSMinX([button frame]) >= maxViewX) + break; + [buttons_ addObject:button]; + } +} + +- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node + xOffset:(int*)xOffset { + BookmarkButtonCell* cell = [self cellForBookmarkNode:node]; + NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset]; + + scoped_nsobject<BookmarkButton> + button([[BookmarkButton alloc] initWithFrame:frame]); + DCHECK(button.get()); + + // [NSButton setCell:] warns to NOT use setCell: other than in the + // initializer of a control. However, we are using a basic + // NSButton whose initializer does not take an NSCell as an + // object. To honor the assumed semantics, we do nothing with + // NSButton between alloc/init and setCell:. + [button setCell:cell]; + [button setDelegate:self]; + + // We cannot set the button cell's text color until it is placed in + // the button (e.g. the [button setCell:cell] call right above). We + // also cannot set the cell's text color until the view is added to + // the hierarchy. If that second part is now true, set the color. + // (If not we'll set the color on the 1st themeChanged: + // notification.) + ThemeProvider* themeProvider = [[[self view] window] themeProvider]; + if (themeProvider) { + NSColor* color = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, + true); + [cell setTextColor:color]; + } + + if (node->is_folder()) { + [button setTarget:self]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; + } else { + // Make the button do something + [button setTarget:self]; + [button setAction:@selector(openBookmark:)]; + // Add a tooltip. + NSString* title = base::SysWideToNSString(node->GetTitle()); + std::string url_string = node->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, + url_string.c_str()]; + [button setToolTip:tooltip]; + } + return [[button.get() retain] autorelease]; +} + +// Add non-bookmark buttons to the view. This includes the chevron +// and the "other bookmarks" button. Technically "other bookmarks" is +// a bookmark button but it is treated specially. Only needs to be +// called when these buttons are new or when the bookmark bar is +// cleared (e.g. on a loaded: call). Unlike addButtonsToView below, +// we don't need to add/remove these dynamically in response to window +// resize. +- (void)addNonBookmarkButtonsToView { + [buttonView_ addSubview:otherBookmarksButton_.get()]; + [buttonView_ addSubview:offTheSideButton_]; +} + +// Add bookmark buttons to the view only if they are completely +// visible and don't overlap the "other bookmarks". Remove buttons +// which are clipped. Called when building the bookmark bar the first time. +- (void)addButtonsToView { + displayedButtonCount_ = 0; + NSMutableArray* buttons = [self buttons]; + for (NSButton* button in buttons) { + if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) - + bookmarks::kBookmarkHorizontalPadding)) + break; + [buttonView_ addSubview:button]; + ++displayedButtonCount_; + } + NSUInteger removalCount = + [buttons count] - (NSUInteger)displayedButtonCount_; + if (removalCount > 0) { + NSRange removalRange = NSMakeRange(displayedButtonCount_, removalCount); + [buttons removeObjectsInRange:removalRange]; + } +} + +// Create the button for "Other Bookmarks" on the right of the bar. +- (void)createOtherBookmarksButton { + // Can't create this until the model is loaded, but only need to + // create it once. + if (otherBookmarksButton_.get()) + return; + + // TODO(jrg): remove duplicate code + NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()]; + int ignored = 0; + NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:&ignored]; + frame.origin.x = [[self buttonView] bounds].size.width - frame.size.width; + frame.origin.x -= bookmarks::kBookmarkHorizontalPadding; + BookmarkButton* button = [[BookmarkButton alloc] initWithFrame:frame]; + [button setDraggable:NO]; + otherBookmarksButton_.reset(button); + view_id_util::SetID(button, VIEW_ID_OTHER_BOOKMARKS); + + // Make sure this button, like all other BookmarkButtons, lives + // until the end of the current event loop. + [[button retain] autorelease]; + + // Peg at right; keep same height as bar. + [button setAutoresizingMask:(NSViewMinXMargin)]; + [button setCell:cell]; + [button setDelegate:self]; + [button setTarget:self]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; + [buttonView_ addSubview:button]; + + // Now that it's here, move the chevron over. + [self positionOffTheSideButton]; +} + +// Now that the model is loaded, set the bookmark bar root as the node +// represented by the bookmark bar (default, background) menu. +- (void)setNodeForBarMenu { + const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); + BookmarkMenu* menu = static_cast<BookmarkMenu*>([[self view] menu]); + + // Make sure types are compatible + DCHECK(sizeof(long long) == sizeof(int64)); + [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]]; +} + +// To avoid problems with sync, changes that may impact the current +// bookmark (e.g. deletion) make sure context menus are closed. This +// prevents deleting a node which no longer exists. +- (void)cancelMenuTracking { + [buttonContextMenu_ cancelTracking]; + [buttonFolderContextMenu_ cancelTracking]; +} + +// Determines the appropriate state for the given situation. ++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar { + if (showNormalBar) + return bookmarks::kShowingState; + if (showDetachedBar) + return bookmarks::kDetachedState; + return bookmarks::kHiddenState; +} + +- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState + withAnimation:(BOOL)animate { + BOOL isAnimationRunning = [self isAnimationRunning]; + + // No-op if the next state is the same as the "current" one, subject to the + // following conditions: + // - no animation is running; or + // - an animation is running and |animate| is YES ([*] if it's NO, we'd want + // to cancel the animation and jump to the final state). + if ((nextVisualState == visualState_) && (!isAnimationRunning || animate)) + return; + + // If an animation is running, we want to finalize it. Otherwise we'd have to + // be able to animate starting from the middle of one type of animation. We + // assume that animations that we know about can be "reversed". + if (isAnimationRunning) { + // Don't cancel if we're going to reverse the animation. + if (nextVisualState != lastVisualState_) { + [self stopCurrentAnimation]; + [self finalizeVisualState]; + } + + // If we're in case [*] above, we can stop here. + if (nextVisualState == visualState_) + return; + } + + // Now update with the new state change. + lastVisualState_ = visualState_; + visualState_ = nextVisualState; + + // Animate only if told to and if bar is enabled. + if (animate && !ignoreAnimations_ && barIsEnabled_) { + [self closeAllBookmarkFolders]; + // Take care of any animation cases we know how to handle. + + // We know how to handle hidden <-> normal, normal <-> detached.... + if ([self isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState] || + [self isAnimatingBetweenState:bookmarks::kShowingState + andState:bookmarks::kDetachedState]) { + [delegate_ bookmarkBar:self willAnimateFromState:lastVisualState_ + toState:visualState_]; + [self showBookmarkBarWithAnimation:YES]; + return; + } + + // If we ever need any other animation cases, code would go here. + // Let any animation cases which we don't know how to handle fall through to + // the unanimated case. + } + + // Just jump to the state. + [self finalizeVisualState]; +} + +// N.B.: |-moveToVisualState:...| will check if this should be a no-op or not. +- (void)updateAndShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar + withAnimation:(BOOL)animate { + bookmarks::VisualState newVisualState = + [BookmarkBarController visualStateToShowNormalBar:showNormalBar + showDetachedBar:showDetachedBar]; + [self moveToVisualState:newVisualState + withAnimation:animate && !ignoreAnimations_]; +} + +// (Private) +- (void)finalizeVisualState { + // We promise that our delegate that the variables will be finalized before + // the call to |-bookmarkBar:didChangeFromState:toState:|. + bookmarks::VisualState oldVisualState = lastVisualState_; + lastVisualState_ = bookmarks::kInvalidState; + + // Notify our delegate. + [delegate_ bookmarkBar:self didChangeFromState:oldVisualState + toState:visualState_]; + + // Update ourselves visually. + [self updateVisibility]; +} + +// (Private) +- (void)stopCurrentAnimation { + [[self animatableView] stopAnimation]; +} + +// Delegate method for |AnimatableView| (a superclass of +// |BookmarkBarToolbarView|). +- (void)animationDidEnd:(NSAnimation*)animation { + [self finalizeVisualState]; +} + +- (void)reconfigureBookmarkBar { + [self redistributeButtonsOnBarAsNeeded]; + [self positionOffTheSideButton]; + [self configureOffTheSideButtonContentsAndVisibility]; + [self centerNoItemsLabel]; +} + +// Determine if the given |view| can completely fit within the constraint of +// maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum +// width. If the minimum width is not achievable then hide the view. Return YES +// if the view was hidden. +- (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX { + BOOL wasHidden = NO; + // See if the view needs to be narrowed. + NSRect frame = [view frame]; + if (NSMaxX(frame) > maxViewX) { + // Resize if more than 30 pixels are showing, otherwise hide. + if (NSMinX(frame) + 30.0 < maxViewX) { + frame.size.width = maxViewX - NSMinX(frame); + [view setFrame:frame]; + } else { + [view setHidden:YES]; + wasHidden = YES; + } + } + return wasHidden; +} + +// Adjust the horizontal width and the visibility of the "For quick access" +// text field and "Import bookmarks..." button based on the current width +// of the containing |buttonView_| (which is affected by window width). +- (void)adjustNoItemContainerWidthsForMaxX:(CGFloat)maxViewX { + if (![[buttonView_ noItemContainer] isHidden]) { + // Reset initial frames for the two items, then adjust as necessary. + NSTextField* noItemTextfield = [buttonView_ noItemTextfield]; + [noItemTextfield setFrame:originalNoItemsRect_]; + [noItemTextfield setHidden:NO]; + NSButton* importBookmarksButton = [buttonView_ importBookmarksButton]; + [importBookmarksButton setFrame:originalImportBookmarksRect_]; + [importBookmarksButton setHidden:NO]; + // Check each to see if they need to be shrunk or hidden. + if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX]) + [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX]; + } +} + +- (void)redistributeButtonsOnBarAsNeeded { + const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); + NSInteger barCount = node->GetChildCount(); + + // Determine the current maximum extent of the visible buttons. + CGFloat maxViewX = NSMaxX([[self view] bounds]); + NSButton* otherBookmarksButton = otherBookmarksButton_.get(); + // If necessary, pull in the width to account for the Other Bookmarks button. + if (otherBookmarksButton_) + maxViewX = [otherBookmarksButton frame].origin.x - + bookmarks::kBookmarkHorizontalPadding; + // If we're already overflowing, then we need to account for the chevron. + if (barCount > displayedButtonCount_) + maxViewX = [offTheSideButton_ frame].origin.x - + bookmarks::kBookmarkHorizontalPadding; + + // As a result of pasting or dragging, the bar may now have more buttons + // than will fit so remove any which overflow. They will be shown in + // the off-the-side folder. + while (displayedButtonCount_ > 0) { + BookmarkButton* button = [buttons_ lastObject]; + if (NSMaxX([button frame]) < maxViewX) + break; + [buttons_ removeLastObject]; + [button removeFromSuperview]; + --displayedButtonCount_; + } + + // As a result of cutting, deleting and dragging, the bar may now have room + // for more buttons. + int xOffset = displayedButtonCount_ > 0 ? + NSMaxX([[buttons_ lastObject] frame]) + + bookmarks::kBookmarkHorizontalPadding : 0; + for (int i = displayedButtonCount_; i < barCount; ++i) { + const BookmarkNode* child = node->GetChild(i); + BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; + // If we're testing against the last possible button then account + // for the chevron no longer needing to be shown. + if (i == barCount + 1) + maxViewX += NSWidth([offTheSideButton_ frame]) + + bookmarks::kBookmarkHorizontalPadding; + if (NSMaxX([button frame]) >= maxViewX) + break; + ++displayedButtonCount_; + [buttons_ addObject:button]; + [buttonView_ addSubview:button]; + } + + // While we're here, adjust the horizontal width and the visibility + // of the "For quick access" and "Import bookmarks..." text fields. + if (![buttons_ count]) + [self adjustNoItemContainerWidthsForMaxX:maxViewX]; +} + +#pragma mark Private Methods Exposed for Testing + +- (BookmarkBarView*)buttonView { + return buttonView_; +} + +- (NSMutableArray*)buttons { + return buttons_.get(); +} + +- (NSButton*)offTheSideButton { + return offTheSideButton_; +} + +- (BOOL)offTheSideButtonIsHidden { + return [offTheSideButton_ isHidden]; +} + +- (NSButton*)otherBookmarksButton { + return otherBookmarksButton_.get(); +} + +- (BookmarkBarFolderController*)folderController { + return folderController_; +} + +- (id)folderTarget { + return folderTarget_.get(); +} + +- (int)displayedButtonCount { + return displayedButtonCount_; +} + +// Delete all buttons (bookmarks, chevron, "other bookmarks") from the +// bookmark bar; reset knowledge of bookmarks. +- (void)clearBookmarkBar { + [buttons_ makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [buttons_ removeAllObjects]; + [self clearMenuTagMap]; + displayedButtonCount_ = 0; + + // Make sure there are no stale pointers in the pasteboard. This + // can be important if a bookmark is deleted (via bookmark sync) + // while in the middle of a drag. The "drag completed" code + // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is + // careful enough to bail if there is no data found at "drop" time. + // + // Unfortunately the clearContents selector is 10.6 only. The best + // we can do is make sure something else is present in place of the + // stale bookmark. + NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; + [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self]; + [pboard setString:@"" forType:NSStringPboardType]; +} + +// Return an autoreleased NSCell suitable for a bookmark button. +// TODO(jrg): move much of the cell config into the BookmarkButtonCell class. +- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node { + NSImage* image = node ? [self favIconForNode:node] : nil; + NSMenu* menu = node && node->is_folder() ? buttonFolderContextMenu_ : + buttonContextMenu_; + BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node + contextMenu:menu + cellText:nil + cellImage:image]; + [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; + + // Note: a quirk of setting a cell's text color is that it won't work + // until the cell is associated with a button, so we can't theme the cell yet. + + return cell; +} + +// Returns a frame appropriate for the given bookmark cell, suitable +// for creating an NSButton that will contain it. |xOffset| is the X +// offset for the frame; it is increased to be an appropriate X offset +// for the next button. +- (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell + xOffset:(int*)xOffset { + DCHECK(xOffset); + NSRect bounds = [buttonView_ bounds]; + bounds.size.height = bookmarks::kBookmarkButtonHeight; + + NSRect frame = NSInsetRect(bounds, + bookmarks::kBookmarkHorizontalPadding, + bookmarks::kBookmarkVerticalPadding); + frame.size.width = [self widthForBookmarkButtonCell:cell]; + + // Add an X offset based on what we've already done + frame.origin.x += *xOffset; + + // And up the X offset for next time. + *xOffset = NSMaxX(frame); + + return frame; +} + +// A bookmark button's contents changed. Check for growth +// (e.g. increase the width up to the maximum). If we grew, move +// other bookmark buttons over. +- (void)checkForBookmarkButtonGrowth:(NSButton*)button { + NSRect frame = [button frame]; + CGFloat desiredSize = [self widthForBookmarkButtonCell:[button cell]]; + CGFloat delta = desiredSize - frame.size.width; + if (delta) { + frame.size.width = desiredSize; + [button setFrame:frame]; + for (NSButton* button in buttons_.get()) { + NSRect buttonFrame = [button frame]; + if (buttonFrame.origin.x > frame.origin.x) { + buttonFrame.origin.x += delta; + [button setFrame:buttonFrame]; + } + } + } + // We may have just crossed a threshold to enable the off-the-side + // button. + [self configureOffTheSideButtonContentsAndVisibility]; +} + +// Called when our controlled frame has changed size. +- (void)frameDidChange { + if (!bookmarkModel_->IsLoaded()) + return; + [self updateTheme:[[[self view] window] themeProvider]]; + [self reconfigureBookmarkBar]; +} + +// Given a NSMenuItem tag, return the appropriate bookmark node id. +- (int64)nodeIdFromMenuTag:(int32)tag { + return menuTagMap_[tag]; +} + +// Create and return a new tag for the given node id. +- (int32)menuTagFromNodeId:(int64)menuid { + int tag = seedId_++; + menuTagMap_[tag] = menuid; + return tag; +} + +// Return the BookmarkNode associated with the given NSMenuItem. Can +// return NULL which means "do nothing". One case where it would +// return NULL is if the bookmark model gets modified while you have a +// context menu open. +- (const BookmarkNode*)nodeFromMenuItem:(id)sender { + const BookmarkNode* node = NULL; + BookmarkMenu* menu = (BookmarkMenu*)[sender menu]; + if ([menu isKindOfClass:[BookmarkMenu class]]) { + int64 id = [menu id]; + node = bookmarkModel_->GetNodeByID(id); + } + return node; +} + +// Adapt appearance of buttons to the current theme. Called after +// theme changes, or when our view is added to the view hierarchy. +// Oddly, the view pings us instead of us pinging our view. This is +// because our trigger is an [NSView viewWillMoveToWindow:], which the +// controller doesn't normally know about. Otherwise we don't have +// access to the theme before we know what window we will be on. +- (void)updateTheme:(ThemeProvider*)themeProvider { + if (!themeProvider) + return; + NSColor* color = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, + true); + for (BookmarkButton* button in buttons_.get()) { + BookmarkButtonCell* cell = [button cell]; + [cell setTextColor:color]; + } + [[otherBookmarksButton_ cell] setTextColor:color]; +} + +// Return YES if the event indicates an exit from the bookmark bar +// folder menus. E.g. "click outside" of the area we are watching. +// At this time we are watching the area that includes all popup +// bookmark folder windows. +- (BOOL)isEventAnExitEvent:(NSEvent*)event { + NSWindow* eventWindow = [event window]; + NSWindow* myWindow = [[self view] window]; + switch ([event type]) { + case NSLeftMouseDown: + case NSRightMouseDown: + // If the click is in my window but NOT in the bookmark bar, consider + // it a click 'outside'. Clicks directly on an active button (i.e. one + // that is a folder and for which its folder menu is showing) are 'in'. + // All other clicks on the bookmarks bar are counted as 'outside' + // because they should close any open bookmark folder menu. + if (eventWindow == myWindow) { + NSView* hitView = + [[eventWindow contentView] hitTest:[event locationInWindow]]; + if (hitView == [folderController_ parentButton]) + return NO; + if (![hitView isDescendantOf:[self view]] || hitView == buttonView_) + return YES; + } + // If a click in a bookmark bar folder window and that isn't + // one of my bookmark bar folders, YES is click outside. + if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow + class]]) { + return YES; + } + break; + case NSKeyDown: + case NSKeyUp: + // Any key press ends things. + return YES; + default: + break; + } + return NO; +} + +#pragma mark Drag & Drop + +// Find something like std::is_between<T>? I can't believe one doesn't exist. +static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { + return ((value >= low) && (value <= high)); +} + +// Return the proposed drop target for a hover open button from the +// given array, or nil if none. We use this for distinguishing +// between a hover-open candidate or drop-indicator draw. +// Helper for buttonForDroppingOnAtPoint:. +// Get UI review on "middle half" ness. +// http://crbug.com/36276 +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point + fromArray:(NSArray*)array { + for (BookmarkButton* button in array) { + // Break early if we've gone too far. + if ((NSMinX([button frame]) > point.x) || (![button superview])) + return nil; + // Careful -- this only applies to the bar with horiz buttons. + // Intentionally NOT using NSPointInRect() so that scrolling into + // a submenu doesn't cause it to be closed. + if (ValueInRangeInclusive(NSMinX([button frame]), + point.x, + NSMaxX([button frame]))) { + // Over a button but let's be a little more specific (make sure + // it's over the middle half, not just over it). + NSRect frame = [button frame]; + NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0); + if (ValueInRangeInclusive(NSMinX(middleHalfOfButton), + point.x, + NSMaxX(middleHalfOfButton))) { + // It makes no sense to drop on a non-folder; there is no hover. + if (![button isFolder]) + return nil; + // Got it! + return button; + } else { + // Over a button but not over the middle half. + return nil; + } + } + } + // Not hovering over a button. + return nil; +} + +// Return the proposed drop target for a hover open button, or nil if +// none. Works with both the bookmark buttons and the "Other +// Bookmarks" button. Point is in [self view] coordinates. +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { + point = [[self view] convertPoint:point + fromView:[[[self view] window] contentView]]; + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point + fromArray:buttons_.get()]; + // One more chance -- try "Other Bookmarks" and "off the side" (if visible). + // This is different than BookmarkBarFolderController. + if (!button) { + NSMutableArray* array = [NSMutableArray array]; + if (![self offTheSideButtonIsHidden]) + [array addObject:offTheSideButton_]; + [array addObject:otherBookmarksButton_]; + button = [self buttonForDroppingOnAtPoint:point + fromArray:array]; + } + return button; +} + +- (int)indexForDragToPoint:(NSPoint)point { + // TODO(jrg): revisit position info based on UI team feedback. + // dropLocation is in bar local coordinates. + NSPoint dropLocation = + [[self view] convertPoint:point + fromView:[[[self view] window] contentView]]; + BookmarkButton* buttonToTheRightOfDraggedButton = nil; + for (BookmarkButton* button in buttons_.get()) { + CGFloat midpoint = NSMidX([button frame]); + if (dropLocation.x <= midpoint) { + buttonToTheRightOfDraggedButton = button; + break; + } + } + if (buttonToTheRightOfDraggedButton) { + const BookmarkNode* afterNode = + [buttonToTheRightOfDraggedButton bookmarkNode]; + DCHECK(afterNode); + int index = afterNode->GetParent()->IndexOfChild(afterNode); + // Make sure we don't get confused by buttons which aren't visible. + return std::min(index, displayedButtonCount_); + } + + // If nothing is to my right I am at the end! + return displayedButtonCount_; +} + +// TODO(mrossetti,jrg): Yet more duplicated code. +// http://crbug.com/35966 +- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode + to:(NSPoint)point + copy:(BOOL)copy { + DCHECK(sourceNode); + // Drop destination. + const BookmarkNode* destParent = NULL; + int destIndex = 0; + + // First check if we're dropping on a button. If we have one, and + // it's a folder, drop in it. + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + if ([button isFolder]) { + destParent = [button bookmarkNode]; + // Drop it at the end. + destIndex = [button bookmarkNode]->GetChildCount(); + } else { + // Else we're dropping somewhere on the bar, so find the right spot. + destParent = bookmarkModel_->GetBookmarkBarNode(); + destIndex = [self indexForDragToPoint:point]; + } + + // Be sure we don't try and drop a folder into itself. + if (sourceNode != destParent) { + if (copy) + bookmarkModel_->Copy(sourceNode, destParent, destIndex); + else + bookmarkModel_->Move(sourceNode, destParent, destIndex); + } + + [self closeFolderAndStopTrackingMenus]; + + // Movement of a node triggers observers (like us) to rebuild the + // bar so we don't have to do so explicitly. + + return YES; +} + +- (void)draggingEnded:(id<NSDraggingInfo>)info { + [self closeFolderAndStopTrackingMenus]; +} + +#pragma mark Bridge Notification Handlers + +// TODO(jrg): for now this is brute force. +- (void)loaded:(BookmarkModel*)model { + DCHECK(model == bookmarkModel_); + if (!model->IsLoaded()) + return; + + // If this is a rebuild request while we have a folder open, close it. + // TODO(mrossetti): Eliminate the need for this because it causes the folder + // menu to disappear after a cut/copy/paste/delete change. + // See: http://crbug.com/36614 + if (folderController_) + [self closeAllBookmarkFolders]; + + // Brute force nuke and build. + savedFrameWidth_ = NSWidth([[self view] frame]); + const BookmarkNode* node = model->GetBookmarkBarNode(); + [self clearBookmarkBar]; + [self addNodesToButtonList:node]; + [self createOtherBookmarksButton]; + [self updateTheme:[[[self view] window] themeProvider]]; + [self positionOffTheSideButton]; + [self addNonBookmarkButtonsToView]; + [self addButtonsToView]; + [self configureOffTheSideButtonContentsAndVisibility]; + [self setNodeForBarMenu]; +} + +- (void)beingDeleted:(BookmarkModel*)model { + // The browser may be being torn down; little is safe to do. As an + // example, it may not be safe to clear the pasteboard. + // http://crbug.com/38665 +} + +- (void)nodeAdded:(BookmarkModel*)model + parent:(const BookmarkNode*)newParent index:(int)newIndex { + // If a context menu is open, close it. + [self cancelMenuTracking]; + + const BookmarkNode* newNode = newParent->GetChild(newIndex); + id<BookmarkButtonControllerProtocol> newController = + [self controllerForNode:newParent]; + [newController addButtonForNode:newNode atIndex:newIndex]; + // If we go from 0 --> 1 bookmarks we may need to hide the + // "bookmarks go here" text container. + [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; +} + +// TODO(jrg): for now this is brute force. +- (void)nodeChanged:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [self loaded:model]; +} + +- (void)nodeMoved:(BookmarkModel*)model + oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex + newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex { + const BookmarkNode* movedNode = newParent->GetChild(newIndex); + id<BookmarkButtonControllerProtocol> oldController = + [self controllerForNode:oldParent]; + id<BookmarkButtonControllerProtocol> newController = + [self controllerForNode:newParent]; + if (newController == oldController) { + [oldController moveButtonFromIndex:oldIndex toIndex:newIndex]; + } else { + [oldController removeButton:oldIndex animate:NO]; + [newController addButtonForNode:movedNode atIndex:newIndex]; + } + // If the bar is one of the parents we may need to update the visibility + // of the "bookmarks go here" presentation. + [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; + // If we moved the only item on the "off the side" menu somewhere + // else, we may no longer need to show it. + [self configureOffTheSideButtonContentsAndVisibility]; +} + +- (void)nodeRemoved:(BookmarkModel*)model + parent:(const BookmarkNode*)oldParent index:(int)index { + // If a context menu is open, close it. + [self cancelMenuTracking]; + + // Locate the parent node. The parent may not be showing, in which case + // we do nothing. + id<BookmarkButtonControllerProtocol> parentController = + [self controllerForNode:oldParent]; + [parentController removeButton:index animate:YES]; + // If we go from 1 --> 0 bookmarks we may need to show the + // "bookmarks go here" text container. + [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; + // If we deleted the only item on the "off the side" menu we no + // longer need to show it. + [self configureOffTheSideButtonContentsAndVisibility]; +} + +// TODO(jrg): linear searching is bad. +// Need a BookmarkNode-->NSCell mapping. +// +// TODO(jrg): if the bookmark bar is open on launch, we see the +// buttons all placed, then "scooted over" as the favicons load. If +// this looks bad I may need to change widthForBookmarkButtonCell to +// add space for an image even if not there on the assumption that +// favicons will eventually load. +- (void)nodeFavIconLoaded:(BookmarkModel*)model + node:(const BookmarkNode*)node { + for (BookmarkButton* button in buttons_.get()) { + const BookmarkNode* cellnode = [button bookmarkNode]; + if (cellnode == node) { + [[button cell] setBookmarkCellText:nil + image:[self favIconForNode:node]]; + // Adding an image means we might need more room for the + // bookmark. Test for it by growing the button (if needed) + // and shifting everything else over. + [self checkForBookmarkButtonGrowth:button]; + } + } +} + +// TODO(jrg): for now this is brute force. +- (void)nodeChildrenReordered:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [self loaded:model]; +} + +#pragma mark BookmarkBarState Protocol + +// (BookmarkBarState protocol) +- (BOOL)isVisible { + return barIsEnabled_ && (visualState_ == bookmarks::kShowingState || + visualState_ == bookmarks::kDetachedState || + lastVisualState_ == bookmarks::kShowingState || + lastVisualState_ == bookmarks::kDetachedState); +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimationRunning { + return lastVisualState_ != bookmarks::kInvalidState; +} + +// (BookmarkBarState protocol) +- (BOOL)isInState:(bookmarks::VisualState)state { + return visualState_ == state && + lastVisualState_ == bookmarks::kInvalidState; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingToState:(bookmarks::VisualState)state { + return visualState_ == state && + lastVisualState_ != bookmarks::kInvalidState; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state { + return lastVisualState_ == state; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState + toState:(bookmarks::VisualState)toState { + return lastVisualState_ == fromState && visualState_ == toState; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState + andState:(bookmarks::VisualState)toState { + return (lastVisualState_ == fromState && visualState_ == toState) || + (visualState_ == fromState && lastVisualState_ == toState); +} + +// (BookmarkBarState protocol) +- (CGFloat)detachedMorphProgress { + if ([self isInState:bookmarks::kDetachedState]) { + return 1; + } + if ([self isAnimatingToState:bookmarks::kDetachedState]) { + return static_cast<CGFloat>( + [[self animatableView] currentAnimationProgress]); + } + if ([self isAnimatingFromState:bookmarks::kDetachedState]) { + return static_cast<CGFloat>( + 1 - [[self animatableView] currentAnimationProgress]); + } + return 0; +} + +#pragma mark BookmarkBarToolbarViewController Protocol + +- (int)currentTabContentsHeight { + return browser_->GetSelectedTabContents() ? + browser_->GetSelectedTabContents()->view()->GetContainerSize().height() : + 0; +} + +- (ThemeProvider*)themeProvider { + return browser_->profile()->GetThemeProvider(); +} + +#pragma mark BookmarkButtonDelegate Protocol + +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button { + [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; +} + +// BookmarkButtonDelegate protocol implementation. When menus are +// "active" (e.g. you clicked to open one), moving the mouse over +// another folder button should close the 1st and open the 2nd (like +// real menus). We detect and act here. +- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { + DCHECK([sender isKindOfClass:[BookmarkButton class]]); + + // If folder menus are not being shown, do nothing. This is different from + // BookmarkBarFolderController's implementation because the bar should NOT + // automatically open folder menus when the mouse passes over a folder + // button while the BookmarkBarFolderController DOES automically open + // a subfolder menu. + if (!showFolderMenus_) + return; + + // From here down: same logic as BookmarkBarFolderController. + // TODO(jrg): find a way to share these 4 non-comment lines? + // http://crbug.com/35966 + // If already opened, then we exited but re-entered the button, so do nothing. + if ([folderController_ parentButton] == sender) + return; + // Else open a new one if it makes sense to do so. + if ([sender bookmarkNode]->is_folder()) { + [folderTarget_ openBookmarkFolderFromButton:sender]; + } else { + // We're over a non-folder bookmark so close any old folders. + [folderController_ close]; + folderController_ = nil; + } +} + +// BookmarkButtonDelegate protocol implementation. +- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { + // Don't care; do nothing. + // This is different behavior that the folder menus. +} + +- (NSWindow*)browserWindow { + return [[self view] window]; +} + +#pragma mark BookmarkButtonControllerProtocol + +// Close all bookmark folders. "Folder" here is the fake menu for +// bookmark folders, not a button context menu. +- (void)closeAllBookmarkFolders { + [self watchForExitEvent:NO]; + [folderController_ close]; + folderController_ = nil; +} + +- (void)closeBookmarkFolder:(id)sender { + // We're the top level, so close one means close them all. + [self closeAllBookmarkFolders]; +} + +- (BookmarkModel*)bookmarkModel { + return bookmarkModel_; +} + +// TODO(jrg): much of this logic is duped with +// [BookmarkBarFolderController draggingEntered:] except when noted. +// http://crbug.com/35966 +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + NSPoint point = [info draggingLocation]; + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + + // Don't allow drops that would result in cycles. + if (button) { + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + if (data && [info draggingSource]) { + BookmarkButton* sourceButton = nil; + [data getBytes:&sourceButton length:sizeof(sourceButton)]; + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + const BookmarkNode* destNode = [button bookmarkNode]; + if (destNode->HasAncestor(sourceNode)) + button = nil; + } + } + + if ([button isFolder]) { + if (hoverButton_ == button) { + return NSDragOperationMove; // already open or timed to open + } + if (hoverButton_) { + // Oops, another one triggered or open. + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ + target]]; + // Unlike BookmarkBarFolderController, we do not delay the close + // of the previous one. Given the lack of diagonal movement, + // there is no need, and it feels awkward to do so. See + // comments about kDragHoverCloseDelay in + // bookmark_bar_folder_controller.mm for more details. + [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; + hoverButton_.reset(); + } + hoverButton_.reset([button retain]); + DCHECK([[hoverButton_ target] + respondsToSelector:@selector(openBookmarkFolderFromButton:)]); + [[hoverButton_ target] + performSelector:@selector(openBookmarkFolderFromButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverOpenDelay]; + } + if (!button) { + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; + hoverButton_.reset(); + } + } + + // Thrown away but kept to be consistent with the draggingEntered: interface. + return NSDragOperationMove; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + // NOT the same as a cancel --> we may have moved the mouse into the submenu. + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + hoverButton_.reset(); + } +} + +- (BOOL)dragShouldLockBarVisibility { + return ![self isInState:bookmarks::kDetachedState] && + ![self isAnimatingToState:bookmarks::kDetachedState]; +} + +// TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController. +// http://crbug.com/35966 +- (BOOL)dragButton:(BookmarkButton*)sourceButton + to:(NSPoint)point + copy:(BOOL)copy { + DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + return [self dragBookmark:sourceNode to:point copy:copy]; +} + +- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { + BOOL dragged = NO; + std::vector<const BookmarkNode*> nodes([self retrieveBookmarkDragDataNodes]); + if (nodes.size()) { + BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); + NSPoint dropPoint = [info draggingLocation]; + for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin(); + it != nodes.end(); ++it) { + const BookmarkNode* sourceNode = *it; + dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; + } + } + return dragged; +} + +- (std::vector<const BookmarkNode*>)retrieveBookmarkDragDataNodes { + std::vector<const BookmarkNode*> dragDataNodes; + BookmarkDragData dragData; + if(dragData.ReadFromDragClipboard()) { + BookmarkModel* bookmarkModel = [self bookmarkModel]; + Profile* profile = bookmarkModel->profile(); + std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile)); + dragDataNodes.assign(nodes.begin(), nodes.end()); + } + return dragDataNodes; +} + +// Return YES if we should show the drop indicator, else NO. +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return ![self buttonForDroppingOnAtPoint:point]; +} + +// Return the x position for a drop indicator. +- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { + CGFloat x = 0; + int destIndex = [self indexForDragToPoint:point]; + int numButtons = displayedButtonCount_; + + // If it's a drop strictly between existing buttons ... + if (destIndex >= 0 && destIndex < numButtons) { + // ... put the indicator right between the buttons. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + x = buttonFrame.origin.x - 0.5 * bookmarks::kBookmarkHorizontalPadding; + + // If it's a drop at the end (past the last button, if there are any) ... + } else if (destIndex == numButtons) { + // and if it's past the last button ... + if (numButtons > 0) { + // ... find the last button, and put the indicator to its right. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + x = NSMaxX(buttonFrame) + 0.5 * bookmarks::kBookmarkHorizontalPadding; + + // Otherwise, put it right at the beginning. + } else { + x = 0.5 * bookmarks::kBookmarkHorizontalPadding; + } + } else { + NOTREACHED(); + } + + return x; +} + +- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { + // If the bookmarkbar is not in detached mode, lock bar visibility, forcing + // the overlay to stay open when in fullscreen mode. + if (![self isInState:bookmarks::kDetachedState] && + ![self isAnimatingToState:bookmarks::kDetachedState]) { + BrowserWindowController* browserController = + [BrowserWindowController browserWindowControllerForView:[self view]]; + [browserController lockBarVisibilityForOwner:child + withAnimation:NO + delay:NO]; + } +} + +- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { + // Release bar visibility, allowing the overlay to close if in fullscreen + // mode. + BrowserWindowController* browserController = + [BrowserWindowController browserWindowControllerForView:[self view]]; + [browserController releaseBarVisibilityForOwner:child + withAnimation:NO + delay:NO]; +} + +// Add a new folder controller as triggered by the given folder button. +- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { + if (folderController_) + [self closeAllBookmarkFolders]; + + // Folder controller, like many window controllers, owns itself. + folderController_ = + [[BookmarkBarFolderController alloc] initWithParentButton:parentButton + parentController:nil + barController:self]; + [folderController_ showWindow:self]; + + // Only BookmarkBarController has this; the + // BookmarkBarFolderController does not. + [self watchForExitEvent:YES]; +} + +- (void)openAll:(const BookmarkNode*)node + disposition:(WindowOpenDisposition)disposition { + [self closeFolderAndStopTrackingMenus]; + bookmark_utils::OpenAll([[self view] window], + browser_->profile(), + browser_, + node, + disposition); +} + +- (void)addButtonForNode:(const BookmarkNode*)node + atIndex:(NSInteger)buttonIndex { + int newOffset = 0; + if (buttonIndex == -1) + buttonIndex = [buttons_ count]; // New button goes at the end. + if (buttonIndex <= (NSInteger)[buttons_ count]) { + if (buttonIndex) { + BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1]; + NSRect targetFrame = [targetButton frame]; + newOffset = targetFrame.origin.x + NSWidth(targetFrame) + + bookmarks::kBookmarkHorizontalPadding; + } + BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset]; + CGFloat xOffset = + NSWidth([newButton frame]) + bookmarks::kBookmarkHorizontalPadding; + NSUInteger buttonCount = [buttons_ count]; + for (NSUInteger i = buttonIndex; i < buttonCount; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSPoint buttonOrigin = [button frame].origin; + buttonOrigin.x += xOffset; + [button setFrameOrigin:buttonOrigin]; + } + ++displayedButtonCount_; + [buttons_ insertObject:newButton atIndex:buttonIndex]; + [buttonView_ addSubview:newButton]; + + // See if any buttons need to be pushed off to or brought in from the side. + [self reconfigureBookmarkBar]; + } else { + // A button from somewhere else (not the bar) is being moved to the + // off-the-side so insure it gets redrawn if its showing. + [self reconfigureBookmarkBar]; + [folderController_ reconfigureMenu]; + } +} + +// TODO(mrossetti): Duplicate code with BookmarkBarFolderController. +// http://crbug.com/35966 +- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { + DCHECK([urls count] == [titles count]); + BOOL nodesWereAdded = NO; + // Figure out where these new bookmarks nodes are to be added. + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + const BookmarkNode* destParent = NULL; + int destIndex = 0; + if ([button isFolder]) { + destParent = [button bookmarkNode]; + // Drop it at the end. + destIndex = [button bookmarkNode]->GetChildCount(); + } else { + // Else we're dropping somewhere on the bar, so find the right spot. + destParent = bookmarkModel_->GetBookmarkBarNode(); + destIndex = [self indexForDragToPoint:point]; + } + + // Don't add the bookmarks if the destination index shows an error. + if (destIndex >= 0) { + // Create and add the new bookmark nodes. + size_t urlCount = [urls count]; + for (size_t i = 0; i < urlCount; ++i) { + GURL gurl; + const char* string = [[urls objectAtIndex:i] UTF8String]; + if (string) + gurl = GURL(string); + // We only expect to receive valid URLs. + DCHECK(gurl.is_valid()); + if (gurl.is_valid()) { + bookmarkModel_->AddURL(destParent, + destIndex++, + base::SysNSStringToWide([titles + objectAtIndex:i]), + gurl); + nodesWereAdded = YES; + } + } + } + return nodesWereAdded; +} + +// TODO(mrossetti): jrg wants this broken up into smaller functions. +- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { + if (fromIndex != toIndex) { + NSInteger buttonCount = (NSInteger)[buttons_ count]; + if (toIndex == -1) + toIndex = buttonCount; + // See if we have a simple move within the bar, which will be the case if + // both button indexes are in the visible space. + if (fromIndex < buttonCount && toIndex < buttonCount) { + BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; + NSRect movedFrame = [movedButton frame]; + NSPoint toOrigin = movedFrame.origin; + CGFloat xOffset = + NSWidth(movedFrame) + bookmarks::kBookmarkHorizontalPadding; + // Hide the button to reduce flickering while drawing the window. + [movedButton setHidden:YES]; + [buttons_ removeObjectAtIndex:fromIndex]; + if (fromIndex < toIndex) { + // Move the button from left to right within the bar. + BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1]; + NSRect toFrame = [targetButton frame]; + toOrigin.x = toFrame.origin.x - NSWidth(movedFrame) + NSWidth(toFrame); + for (NSInteger i = fromIndex; i < toIndex; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect frame = [button frame]; + frame.origin.x -= xOffset; + [button setFrameOrigin:frame.origin]; + } + } else { + // Move the button from right to left within the bar. + BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex]; + toOrigin = [targetButton frame].origin; + for (NSInteger i = fromIndex - 1; i >= toIndex; --i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.x += xOffset; + [button setFrameOrigin:buttonFrame.origin]; + } + } + [buttons_ insertObject:movedButton atIndex:toIndex]; + [movedButton setFrameOrigin:toOrigin]; + [movedButton setHidden:NO]; + } else if (fromIndex < buttonCount) { + // A button is being removed from the bar and added to off-the-side. + // By now the node has already been inserted into the model so the + // button to be added is represented by |toIndex|. Things get + // complicated because the off-the-side is showing and must be redrawn + // while possibly re-laying out the bookmark bar. + [self removeButton:fromIndex animate:NO]; + [self reconfigureBookmarkBar]; + [folderController_ reconfigureMenu]; + } else if (toIndex < buttonCount) { + // A button is being added to the bar and removed from off-the-side. + // By now the node has already been inserted into the model so the + // button to be added is represented by |toIndex|. + const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); + const BookmarkNode* movedNode = node->GetChild(toIndex); + DCHECK(movedNode); + [self addButtonForNode:movedNode atIndex:toIndex]; + [self reconfigureBookmarkBar]; + } else { + // A button is being moved within the off-the-side. + fromIndex -= buttonCount; + toIndex -= buttonCount; + [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex]; + } + } +} + +- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { + if (buttonIndex < (NSInteger)[buttons_ count]) { + // The button being removed is showing in the bar. + BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; + if (oldButton == [folderController_ parentButton]) { + // If we are deleting a button whose folder is currently open, close it! + [self closeAllBookmarkFolders]; + } + NSRect poofFrame = [oldButton bounds]; + NSPoint poofPoint = NSMakePoint(NSMidX(poofFrame), NSMidY(poofFrame)); + poofPoint = [oldButton convertPoint:poofPoint toView:nil]; + poofPoint = [[oldButton window] convertBaseToScreen:poofPoint]; + NSRect oldFrame = [oldButton frame]; + [oldButton removeFromSuperview]; + if (animate && !ignoreAnimations_ && [self isVisible]) + NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, + NSZeroSize, nil, nil, nil); + CGFloat xOffset = NSWidth(oldFrame) + bookmarks::kBookmarkHorizontalPadding; + [buttons_ removeObjectAtIndex:buttonIndex]; + NSUInteger buttonCount = [buttons_ count]; + for (NSUInteger i = buttonIndex; i < buttonCount; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.x -= xOffset; + [button setFrame:buttonFrame]; + // If this button is showing its menu then we need to move the menu, too. + if (button == [folderController_ parentButton]) + [folderController_ offsetFolderMenuWindow:NSMakeSize(xOffset, 0.0)]; + } + --displayedButtonCount_; + [self reconfigureBookmarkBar]; + } else if (folderController_) { + // The button being removed is in the OTS (off-the-side) and the OTS + // menu is showing so we need to remove the button. + NSInteger index = buttonIndex - displayedButtonCount_; + [folderController_ removeButton:index animate:YES]; + } +} + +- (id<BookmarkButtonControllerProtocol>)controllerForNode: + (const BookmarkNode*)node { + // See if it's in the bar, then if it is in the hierarchy of visible + // folder menus. + if (bookmarkModel_->GetBookmarkBarNode() == node) + return self; + return [folderController_ controllerForNode:node]; +} + +#pragma mark BookmarkButtonControllerProtocol + +// NOT an override of a standard Cocoa call made to NSViewControllers. +- (void)hookForEvent:(NSEvent*)theEvent { + if ([self isEventAnExitEvent:theEvent]) + [self closeFolderAndStopTrackingMenus]; +} + +#pragma mark TestingAPI Only + +- (NSMenu*)buttonContextMenu { + return buttonContextMenu_; +} + +// Intentionally ignores ownership issues; used for testing and we try +// to minimize touching the object passed in (likely a mock). +- (void)setButtonContextMenu:(id)menu { + buttonContextMenu_ = menu; +} + +- (void)setIgnoreAnimations:(BOOL)ignore { + ignoreAnimations_ = ignore; +} + +@end |