Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(874)

Unified Diff: chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm

Issue 8141003: [Mac] Restore the old bookmark menus now that the experiment is over. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm
index 342c30ad62b63dde1b507d04ee6b15a3bb424d28..e237f44659b07c20dcb6655e1ec9a1a86cfcd03e 100644
--- a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm
@@ -5,103 +5,1990 @@
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
#include "base/mac/mac_util.h"
+#include "base/sys_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
-#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
-#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/event_utils.h"
+#include "ui/base/theme_provider.h"
-// Forward-declare symbols that are part of the 10.6 SDK.
-#if !defined(MAC_OS_X_VERSION_10_6) || \
- MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6
+using bookmarks::kBookmarkBarMenuCornerRadius;
+
+namespace {
+
+// Frequency of the scrolling timer in seconds.
+const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1;
+
+// Amount to scroll by per timer fire. We scroll rather slowly; to
+// accomodate we do several at a time.
+const CGFloat kBookmarkBarFolderScrollAmount =
+ 3 * bookmarks::kBookmarkFolderButtonHeight;
+
+// Amount to scroll for each scroll wheel roll.
+const CGFloat kBookmarkBarFolderScrollWheelAmount =
+ 1 * bookmarks::kBookmarkFolderButtonHeight;
+
+// Determining adjustments to the layout of the folder menu window in response
+// to resizing and scrolling relies on many visual factors. The following
+// struct is used to pass around these factors to the several support
+// functions involved in the adjustment calculations and application.
+struct LayoutMetrics {
+ // Metrics applied during the final layout adjustments to the window,
+ // the main visible content view, and the menu content view (i.e. the
+ // scroll view).
+ CGFloat windowLeft;
+ NSSize windowSize;
+ // The proposed and then final scrolling adjustment made to the scrollable
+ // area of the folder menu. This may be modified during the window layout
+ // primarily as a result of hiding or showing the scroll arrows.
+ CGFloat scrollDelta;
+ NSRect windowFrame;
+ NSRect visibleFrame;
+ NSRect scrollerFrame;
+ NSPoint scrollPoint;
+ // The difference between 'could' and 'can' in these next four data members
+ // is this: 'could' represents the previous condition for scrollability
+ // while 'can' represents what the new condition will be for scrollability.
+ BOOL couldScrollUp;
+ BOOL canScrollUp;
+ BOOL couldScrollDown;
+ BOOL canScrollDown;
+ // Determines the optimal time during folder menu layout when the contents
+ // of the button scroll area should be scrolled in order to prevent
+ // flickering.
+ BOOL preScroll;
+
+ // Intermediate metrics used in determining window vertical layout changes.
+ CGFloat deltaWindowHeight;
+ CGFloat deltaWindowY;
+ CGFloat deltaVisibleHeight;
+ CGFloat deltaVisibleY;
+ CGFloat deltaScrollerHeight;
+ CGFloat deltaScrollerY;
+
+ // Convenience metrics used in multiple functions (carried along here in
+ // order to eliminate the need to calculate in multiple places and
+ // reduce the possibility of bugs).
+ CGFloat minimumY;
+ CGFloat oldWindowY;
+ CGFloat folderY;
+ CGFloat folderTop;
+
+ LayoutMetrics(CGFloat windowLeft, NSSize windowSize, CGFloat scrollDelta) :
+ windowLeft(windowLeft),
+ windowSize(windowSize),
+ scrollDelta(scrollDelta),
+ couldScrollUp(NO),
+ canScrollUp(NO),
+ couldScrollDown(NO),
+ canScrollDown(NO),
+ preScroll(NO),
+ deltaWindowHeight(0.0),
+ deltaWindowY(0.0),
+ deltaVisibleHeight(0.0),
+ deltaVisibleY(0.0),
+ deltaScrollerHeight(0.0),
+ deltaScrollerY(0.0),
+ oldWindowY(0.0),
+ folderY(0.0),
+ folderTop(0.0) {}
+};
+
+} // namespace
+
+
+// Required to set the right tracking bounds for our fake menus.
+@interface NSView(Private)
+- (void)_updateTrackingAreas;
+@end
+
+@interface BookmarkBarFolderController(Private)
+- (void)configureWindow;
+- (void)addOrUpdateScrollTracking;
+- (void)removeScrollTracking;
+- (void)endScroll;
+- (void)addScrollTimerWithDelta:(CGFloat)delta;
+
+// Helper function to configureWindow which performs a basic layout of
+// the window subviews, in particular the menu buttons and the window width.
+- (void)layOutWindowWithHeight:(CGFloat)height;
+
+// Determine the best button width (which will be the widest button or the
+// maximum allowable button width, whichever is less) and resize all buttons.
+// Return the new width so that the window can be adjusted.
+- (CGFloat)adjustButtonWidths;
+
+// Returns the total menu height needed to display |buttonCount| buttons.
+// Does not do any fancy tricks like trimming the height to fit on the screen.
+- (int)menuHeightForButtonCount:(int)buttonCount;
+
+// Adjust layout of the folder menu window components, showing/hiding the
+// scroll up/down arrows, and resizing as necessary for a proper disaplay.
+// In order to reduce window flicker, all layout changes are deferred until
+// the final step of the adjustment. To accommodate this deferral, window
+// height and width changes needed by callers to this function pass their
+// desired window changes in |size|. When scrolling is to be performed
+// any scrolling change is given by |scrollDelta|. The ultimate amount of
+// scrolling may be different from |scrollDelta| in order to accommodate
+// changes in the scroller view layout. These proposed window adjustments
+// are passed to helper functions using a LayoutMetrics structure.
+//
+// This function should be called when: 1) initially setting up a folder menu
+// window, 2) responding to scrolling of the contents (which may affect the
+// height of the window), 3) addition or removal of bookmark items (such as
+// during cut/paste/delete/drag/drop operations).
+- (void)adjustWindowLeft:(CGFloat)windowLeft
+ size:(NSSize)windowSize
+ scrollingBy:(CGFloat)scrollDelta;
+
+// Support function for adjustWindowLeft:size:scrollingBy: which initializes
+// the layout adjustments by gathering current folder menu window and subviews
+// positions and sizes. This information is set in the |layoutMetrics|
+// structure.
+- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics;
+
+// Support function for adjustWindowLeft:size:scrollingBy: which calculates
+// the changes which must be applied to the folder menu window and subviews
+// positions and sizes. |layoutMetrics| contains the proposed window size
+// and scrolling along with the other current window and subview layout
+// information. The values in |layoutMetrics| are then adjusted to
+// accommodate scroll arrow presentation and window growth.
+- (void)adjustMetrics:(LayoutMetrics*)layoutMetrics;
+
+// Support function for adjustMetrics: which calculates the layout changes
+// required to accommodate changes in the position and scrollability
+// of the top of the folder menu window.
+- (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics;
+
+// Support function for adjustMetrics: which calculates the layout changes
+// required to accommodate changes in the position and scrollability
+// of the bottom of the folder menu window.
+- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics;
+
+// Support function for adjustWindowLeft:size:scrollingBy: which applies
+// the layout adjustments to the folder menu window and subviews.
+- (void)applyMetrics:(LayoutMetrics*)layoutMetrics;
+
+// This function is called when buttons are added or removed from the folder
+// menu, and which may require a change in the layout of the folder menu
+// window. Such layout changes may include horizontal placement, width,
+// height, and scroller visibility changes. (This function calls through
+// to -[adjustWindowLeft:size:scrollingBy:].)
+// |buttonCount| should contain the updated count of menu buttons.
+- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount;
+
+// A helper function which takes the desired amount to scroll, given by
+// |scrollDelta|, and calculates the actual scrolling change to be applied
+// taking into account the layout of the folder menu window and any
+// changes in it's scrollability. (For example, when scrolling down and the
+// top-most menu item is coming into view we will only scroll enough for
+// that item to be completely presented, which may be less than the
+// scroll amount requested.)
+- (CGFloat)determineFinalScrollDelta:(CGFloat)scrollDelta;
+
+// |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;
-@interface NSMenu (SnowLeopardSDK)
-- (BOOL)popUpMenuPositioningItem:(NSMenuItem*)item
- atLocation:(NSPoint)location
- inView:(NSView*)view;
@end
-#endif // MAC_OS_X_VERSION_10_6
+@interface BookmarkButton (BookmarkBarFolderMenuHighlighting)
+
+// Make the button's border frame always appear when |forceOn| is YES,
+// otherwise only border the button when the mouse is inside the button.
+- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;
+
+@end
+
+@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)
+
+- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
+ [self setShowsBorderOnlyWhileMouseInside:!forceOn];
+ [self setNeedsDisplay];
+}
+
+@end
@implementation BookmarkBarFolderController
+@synthesize subFolderGrowthToRight = subFolderGrowthToRight_;
+
- (id)initWithParentButton:(BookmarkButton*)button
- bookmarkModel:(BookmarkModel*)model
+ parentController:(BookmarkBarFolderController*)parentController
barController:(BookmarkBarController*)barController {
- if ((self = [super init])) {
+ NSString* nibPath =
+ [base::mac::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
parentButton_.reset([button retain]);
- barController_ = barController;
- menu_.reset([[NSMenu alloc] initWithTitle:@""]);
- menuBridge_.reset(new BookmarkMenuBridge([parentButton_ bookmarkNode],
- model->profile(), menu_));
- [menuBridge_->controller() setDelegate:self];
+ selectedIndex_ = -1;
+
+ // We want the button to remain bordered as part of the menu path.
+ [button forceButtonBorderToStayOnAlways:YES];
+
+ parentController_.reset([parentController retain]);
+ if (!parentController_)
+ [self setSubFolderGrowthToRight:YES];
+ else
+ [self setSubFolderGrowthToRight:[parentController
+ subFolderGrowthToRight]];
+ barController_ = barController; // WEAK
+ buttons_.reset([[NSMutableArray alloc] init]);
+ folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
+ [self configureWindow];
+ hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
}
return self;
}
+- (void)dealloc {
+ [self clearInputText];
+
+ // The button is no longer part of the menu path.
+ [parentButton_ forceButtonBorderToStayOnAlways:NO];
+ [parentButton_ setNeedsDisplay];
+
+ [self removeScrollTracking];
+ [self endScroll];
+ [hoverState_ draggingExited];
+
+ // Delegate pattern does not retain; make sure pointers to us are removed.
+ for (BookmarkButton* button in buttons_.get()) {
+ [button setDelegate:nil];
+ [button setTarget:nil];
+ [button setAction:nil];
+ }
+
+ // Note: we don't need to
+ // [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ // Because all of our performSelector: calls use withDelay: which
+ // retains us.
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ NSRect windowFrame = [[self window] frame];
+ NSRect scrollViewFrame = [scrollView_ frame];
+ padding_ = NSWidth(windowFrame) - NSWidth(scrollViewFrame);
+ verticalScrollArrowHeight_ = NSHeight([scrollUpArrowView_ frame]);
+}
+
+// Overriden from NSWindowController to call childFolderWillShow: before showing
+// the window.
+- (void)showWindow:(id)sender {
+ [barController_ childFolderWillShow:self];
+ [super showWindow:sender];
+}
+
+- (int)buttonCount {
+ return [[self buttons] count];
+}
+
- (BookmarkButton*)parentButton {
return parentButton_.get();
}
-- (void)openMenu {
- // Retain self so that whatever created this can forefit ownership if it
- // wants. This call is balanced in |-bookmarkMenuDidClose:|.
- [self retain];
+- (void)offsetFolderMenuWindow:(NSSize)offset {
+ NSWindow* window = [self window];
+ NSRect windowFrame = [window frame];
+ windowFrame.origin.x -= offset.width;
+ windowFrame.origin.y += offset.height; // Yes, in the opposite direction!
+ [window setFrame:windowFrame display:YES];
+ [folderController_ offsetFolderMenuWindow:offset];
+}
+
+- (void)reconfigureMenu {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ for (BookmarkButton* button in buttons_.get()) {
+ [button setDelegate:nil];
+ [button removeFromSuperview];
+ }
+ [buttons_ removeAllObjects];
+ [self configureWindow];
+}
+
+#pragma mark Private Methods
+
+- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
+ NSImage* image = child ? [barController_ faviconForNode:child] : nil;
+ NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil;
+ BookmarkBarFolderButtonCell* cell =
+ [BookmarkBarFolderButtonCell buttonCellForNode:child
+ contextMenu:menu
+ cellText:nil
+ cellImage:image];
+ [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
+ return cell;
+}
+
+// Redirect to our logic shared with BookmarkBarController.
+- (IBAction)openBookmarkFolderFromButton:(id)sender {
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+// Create a bookmark button for the given node using frame.
+//
+// If |node| is NULL this is an "(empty)" button.
+// Does NOT add this button to our button list.
+// Returns an autoreleased button.
+// Adjusts the input frame width as appropriate.
+//
+// TODO(jrg): combine with addNodesToButtonList: code from
+// bookmark_bar_controller.mm, and generalize that to use both x and y
+// offsets.
+// http://crbug.com/35966
+- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node
+ frame:(NSRect)frame {
+ BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
+ DCHECK(cell);
+
+ // We must decide if we draw the folder arrow before we ask the cell
+ // how big it needs to be.
+ if (node && node->is_folder()) {
+ // Warning when combining code with bookmark_bar_controller.mm:
+ // this call should NOT be made for the bar buttons; only for the
+ // subfolder buttons.
+ [cell setDrawFolderArrow:YES];
+ }
+
+ // The "+2" is needed because, sometimes, Cocoa is off by a tad when
+ // returning the value it thinks it needs.
+ CGFloat desired = [cell cellSize].width + 2;
+ // The width is determined from the maximum of the proposed width
+ // (provided in |frame|) or the natural width of the title, then
+ // limited by the abolute minimum and maximum allowable widths.
+ frame.size.width =
+ std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
+ std::max(frame.size.width, desired)),
+ bookmarks::kBookmarkMenuButtonMaximumWidth);
+
+ BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
+ autorelease];
+ DCHECK(button);
+
+ [button setCell:cell];
+ [button setDelegate:self];
+ if (node) {
+ 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.
+ [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
+ [button setAcceptsTrackIn:YES];
+ }
+ } else {
+ [button setEnabled:NO];
+ [button setBordered:NO];
+ }
+ return button;
+}
+
+- (id)folderTarget {
+ return folderTarget_.get();
+}
+
+
+// Our parent controller is another BookmarkBarFolderController, so
+// our window is to the right or left of it. We use a little overlap
+// since it looks much more menu-like than with none. If we would
+// grow off the screen, switch growth to the other direction. Growth
+// direction sticks for folder windows which are descendents of us.
+// If we have tried both directions and neither fits, degrade to a
+// default.
+- (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
+ // We may legitimately need to try two times (growth to right and
+ // left but not in that order). Limit us to three tries in case
+ // the folder window can't fit on either side of the screen; we
+ // don't want to loop forever.
+ CGFloat x;
+ int tries = 0;
+ while (tries < 2) {
+ // Try to grow right.
+ if ([self subFolderGrowthToRight]) {
+ tries++;
+ x = NSMaxX([[parentButton_ window] frame]) -
+ bookmarks::kBookmarkMenuOverlap;
+ // If off the screen, switch direction.
+ if ((x + windowWidth +
+ bookmarks::kBookmarkHorizontalScreenPadding) >
+ NSMaxX([[[self window] screen] visibleFrame])) {
+ [self setSubFolderGrowthToRight:NO];
+ } else {
+ return x;
+ }
+ }
+ // Try to grow left.
+ if (![self subFolderGrowthToRight]) {
+ tries++;
+ x = NSMinX([[parentButton_ window] frame]) +
+ bookmarks::kBookmarkMenuOverlap -
+ windowWidth;
+ // If off the screen, switch direction.
+ if (x < NSMinX([[[self window] screen] visibleFrame])) {
+ [self setSubFolderGrowthToRight:YES];
+ } else {
+ return x;
+ }
+ }
+ }
+ // Unhappy; do the best we can.
+ return NSMaxX([[[self window] screen] visibleFrame]) - windowWidth;
+}
+
+
+// Compute and return the top left point of our window (screen
+// coordinates). The top left is positioned in a manner similar to
+// cascading menus. Windows may grow to either the right or left of
+// their parent (if a sub-folder) so we need to know |windowWidth|.
+- (NSPoint)windowTopLeftForWidth:(int)windowWidth height:(int)windowHeight {
+ CGFloat kMinSqueezedMenuHeight = bookmarks::kBookmarkFolderButtonHeight * 2.0;
+ NSPoint newWindowTopLeft;
+ if (![parentController_ isKindOfClass:[self class]]) {
+ // If we're not popping up from one of ourselves, we must be
+ // popping up from the bookmark bar itself. In this case, start
+ // BELOW the parent button. Our left is the button left; our top
+ // is bottom of button's parent view.
+ NSPoint buttonBottomLeftInScreen =
+ [[parentButton_ window]
+ convertBaseToScreen:[parentButton_
+ convertPoint:NSZeroPoint toView:nil]];
+ NSPoint bookmarkBarBottomLeftInScreen =
+ [[parentButton_ window]
+ convertBaseToScreen:[[parentButton_ superview]
+ convertPoint:NSZeroPoint toView:nil]];
+ newWindowTopLeft = NSMakePoint(
+ buttonBottomLeftInScreen.x + bookmarks::kBookmarkBarButtonOffset,
+ bookmarkBarBottomLeftInScreen.y + bookmarks::kBookmarkBarMenuOffset);
+ // Make sure the window is on-screen; if not, push left. It is
+ // intentional that top level folders "push left" slightly
+ // different than subfolders.
+ NSRect screenFrame = [[[parentButton_ window] screen] visibleFrame];
+ CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
+ if (spillOff > 0.0) {
+ newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
+ NSMinX(screenFrame));
+ }
+ // The menu looks bad when it is squeezed up against the bottom of the
+ // screen and ends up being only a few pixels tall. If it meets the
+ // threshold for this case, instead show the menu above the button.
+ NSRect visFrame = [[[parentButton_ window] screen] visibleFrame];
+ CGFloat availableVerticalSpace = newWindowTopLeft.y -
+ (NSMinY(visFrame) + bookmarks::kScrollWindowVerticalMargin);
+ if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
+ (windowHeight > availableVerticalSpace)) {
+ newWindowTopLeft.y = std::min(
+ newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
+ NSMaxY(visFrame));
+ }
+ } else {
+ // Parent is a folder: expose as much as we can vertically; grow right/left.
+ newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
+ NSPoint topOfWindow = NSMakePoint(0,
+ NSMaxY([parentButton_ frame]) -
+ bookmarks::kBookmarkVerticalPadding);
+ topOfWindow = [[parentButton_ window]
+ convertBaseToScreen:[[parentButton_ superview]
+ convertPoint:topOfWindow toView:nil]];
+ newWindowTopLeft.y = topOfWindow.y;
+ }
+ return newWindowTopLeft;
+}
+
+// Set our window level to the right spot so we're above the menubar, dock, etc.
+// Factored out so we can override/noop in a unit test.
+- (void)configureWindowLevel {
+ [[self window] setLevel:NSPopUpMenuWindowLevel];
+}
+
+- (int)menuHeightForButtonCount:(int)buttonCount {
+ // This does not take into account any padding which may be required at the
+ // top and/or bottom of the window.
+ return (buttonCount * bookmarks::kBookmarkFolderButtonHeight) +
+ 2 * bookmarks::kBookmarkVerticalPadding;
+}
+
+- (void)adjustWindowLeft:(CGFloat)windowLeft
+ size:(NSSize)windowSize
+ scrollingBy:(CGFloat)scrollDelta {
+ // Callers of this function should make adjustments to the vertical
+ // attributes of the folder view only (height, scroll position).
+ // This function will then make appropriate layout adjustments in order
+ // to accommodate screen/dock margins, scroll-up and scroll-down arrow
+ // presentation, etc.
+ // The 4 views whose vertical height and origins may be adjusted
+ // by this function are:
+ // 1) window, 2) visible content view, 3) scroller view, 4) folder view.
+
+ LayoutMetrics layoutMetrics(windowLeft, windowSize, scrollDelta);
+ [self gatherMetrics:&layoutMetrics];
+ [self adjustMetrics:&layoutMetrics];
+ [self applyMetrics:&layoutMetrics];
+}
+
+- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics {
+ LayoutMetrics& metrics(*layoutMetrics);
+ NSWindow* window = [self window];
+ metrics.windowFrame = [window frame];
+ metrics.visibleFrame = [visibleView_ frame];
+ metrics.scrollerFrame = [scrollView_ frame];
+ metrics.scrollPoint = [scrollView_ documentVisibleRect].origin;
+ metrics.scrollPoint.y -= metrics.scrollDelta;
+ metrics.couldScrollUp = ![scrollUpArrowView_ isHidden];
+ metrics.couldScrollDown = ![scrollDownArrowView_ isHidden];
+
+ metrics.deltaWindowHeight = 0.0;
+ metrics.deltaWindowY = 0.0;
+ metrics.deltaVisibleHeight = 0.0;
+ metrics.deltaVisibleY = 0.0;
+ metrics.deltaScrollerHeight = 0.0;
+ metrics.deltaScrollerY = 0.0;
+
+ metrics.minimumY = NSMinY([[window screen] visibleFrame]) +
+ bookmarks::kScrollWindowVerticalMargin;
+ metrics.oldWindowY = NSMinY(metrics.windowFrame);
+ metrics.folderY =
+ metrics.scrollerFrame.origin.y + metrics.visibleFrame.origin.y +
+ metrics.oldWindowY - metrics.scrollPoint.y;
+ metrics.folderTop = metrics.folderY + NSHeight([folderView_ frame]);
+}
+
+- (void)adjustMetrics:(LayoutMetrics*)layoutMetrics {
+ LayoutMetrics& metrics(*layoutMetrics);
+ NSScreen* screen = [[self window] screen];
+ CGFloat effectiveFolderY = metrics.folderY;
+ if (!metrics.couldScrollUp && !metrics.couldScrollDown)
+ effectiveFolderY -= metrics.windowSize.height;
+ metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
+ CGFloat maximumY =
+ NSMaxY([screen visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
+ metrics.canScrollDown = metrics.folderTop > maximumY;
+
+ // Accommodate changes in the bottom of the menu.
+ [self adjustMetricsForMenuBottomChanges:layoutMetrics];
+
+ // Accommodate changes in the top of the menu.
+ [self adjustMetricsForMenuTopChanges:layoutMetrics];
+
+ metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
+ metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
+ metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
+ metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
+ metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
+ metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
+ metrics.windowFrame.origin.y += metrics.deltaWindowY;
+ metrics.windowFrame.origin.x = metrics.windowLeft;
+ metrics.windowFrame.size.height += metrics.deltaWindowHeight;
+ metrics.windowFrame.size.width = metrics.windowSize.width;
+}
+
+- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
+ LayoutMetrics& metrics(*layoutMetrics);
+ if (metrics.canScrollUp) {
+ if (!metrics.couldScrollUp) {
+ // Couldn't -> Can
+ metrics.deltaWindowY = -metrics.oldWindowY;
+ metrics.deltaWindowHeight = -metrics.deltaWindowY;
+ metrics.deltaVisibleY = metrics.minimumY;
+ metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
+ metrics.deltaScrollerY = verticalScrollArrowHeight_;
+ metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
+ // Adjust the scroll delta if we've grown the window and it is
+ // now scroll-up-able, but don't adjust it if we've
+ // scrolled down and it wasn't scroll-up-able but now is.
+ if (metrics.canScrollDown == metrics.couldScrollDown) {
+ CGFloat deltaScroll = metrics.deltaWindowY + metrics.deltaScrollerY +
+ metrics.deltaVisibleY;
+ metrics.scrollPoint.y += deltaScroll + metrics.windowSize.height;
+ }
+ } else if (!metrics.canScrollDown && metrics.windowSize.height > 0.0) {
+ metrics.scrollPoint.y += metrics.windowSize.height;
+ }
+ } else {
+ if (metrics.couldScrollUp) {
+ // Could -> Can't
+ metrics.deltaWindowY = metrics.folderY - metrics.oldWindowY;
+ metrics.deltaWindowHeight = -metrics.deltaWindowY;
+ metrics.deltaVisibleY = -metrics.visibleFrame.origin.y;
+ metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
+ metrics.deltaScrollerY = -verticalScrollArrowHeight_;
+ metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
+ // We are no longer scroll-up-able so the scroll point drops to zero.
+ metrics.scrollPoint.y = 0.0;
+ } else {
+ // Couldn't -> Can't
+ // Check for menu height change by looking at the relative tops of the
+ // menu folder and the window folder, which previously would have been
+ // the same.
+ metrics.deltaWindowY = NSMaxY(metrics.windowFrame) - metrics.folderTop;
+ metrics.deltaWindowHeight = -metrics.deltaWindowY;
+ }
+ }
+}
- // If the system supports opening the menu at a specific point, do so.
- // Otherwise, it will be opened at the mouse event location. Eventually these
- // should be switched to NSPopUpButtonCells so that this is taken care of
- // automatically.
- if ([menu_ respondsToSelector:
- @selector(popUpMenuPositioningItem:atLocation:inView:)]) {
- NSPoint point = [parentButton_ frame].origin;
- point.y -= bookmarks::kBookmarkBarMenuOffset;
- [menu_ popUpMenuPositioningItem:nil
- atLocation:point
- inView:[parentButton_ superview]];
+- (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics {
+ LayoutMetrics& metrics(*layoutMetrics);
+ if (metrics.canScrollDown == metrics.couldScrollDown) {
+ if (!metrics.canScrollDown) {
+ // Not scroll-down-able but the menu top has changed.
+ metrics.deltaWindowHeight += metrics.scrollDelta;
+ }
} else {
- [NSMenu popUpContextMenu:menu_
- withEvent:[NSApp currentEvent]
- forView:parentButton_];
+ if (metrics.canScrollDown) {
+ // Couldn't -> Can
+ metrics.deltaWindowHeight += (NSMaxY([[[self window] screen]
+ visibleFrame]) -
+ NSMaxY(metrics.windowFrame));
+ metrics.deltaVisibleHeight -= bookmarks::kScrollWindowVerticalMargin;
+ metrics.deltaScrollerHeight -= verticalScrollArrowHeight_;
+ } else {
+ // Could -> Can't
+ metrics.deltaWindowHeight -= bookmarks::kScrollWindowVerticalMargin;
+ metrics.deltaVisibleHeight += bookmarks::kScrollWindowVerticalMargin;
+ metrics.deltaScrollerHeight += verticalScrollArrowHeight_;
+ }
}
}
-- (void)closeMenu {
- NSArray* modes = [NSArray arrayWithObject:NSRunLoopCommonModes];
- [menu_ performSelector:@selector(cancelTracking)
- withObject:nil
- afterDelay:0.0
- inModes:modes];
+- (void)applyMetrics:(LayoutMetrics*)layoutMetrics {
+ LayoutMetrics& metrics(*layoutMetrics);
+ // Hide or show the scroll arrows.
+ if (metrics.canScrollUp != metrics.couldScrollUp)
+ [scrollUpArrowView_ setHidden:metrics.couldScrollUp];
+ if (metrics.canScrollDown != metrics.couldScrollDown)
+ [scrollDownArrowView_ setHidden:metrics.couldScrollDown];
+
+ // Adjust the geometry. The order is important because of sizer dependencies.
+ [scrollView_ setFrame:metrics.scrollerFrame];
+ [visibleView_ setFrame:metrics.visibleFrame];
+ // This little bit of trickery handles the one special case where
+ // the window is now scroll-up-able _and_ going to be resized -- scroll
+ // first in order to prevent flashing.
+ if (metrics.preScroll)
+ [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
+
+ [[self window] setFrame:metrics.windowFrame display:YES];
+
+ // In all other cases we defer scrolling until the window has been resized
+ // in order to prevent flashing.
+ if (!metrics.preScroll)
+ [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
+
+ // TODO(maf) find a non-SPI way to do this.
+ // Hack. This is the only way I've found to get the tracking area cache
+ // to update properly during a mouse tracking loop.
+ // Without this, the item tracking-areas are wrong when using a scrollable
+ // menu with the mouse held down.
+ NSView *contentView = [[self window] contentView] ;
+ if ([contentView respondsToSelector:@selector(_updateTrackingAreas)])
+ [contentView _updateTrackingAreas];
+
+
+ if (metrics.canScrollUp != metrics.couldScrollUp ||
+ metrics.canScrollDown != metrics.couldScrollDown ||
+ metrics.scrollDelta != 0.0) {
+ if (metrics.canScrollUp || metrics.canScrollDown)
+ [self addOrUpdateScrollTracking];
+ else
+ [self removeScrollTracking];
+ }
+}
+
+- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount {
+ NSRect folderFrame = [folderView_ frame];
+ CGFloat newMenuHeight =
+ (CGFloat)[self menuHeightForButtonCount:[buttons_ count]];
+ CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
+ // If the height has changed then also change the origin, and adjust the
+ // scroll (if scrolling).
+ if ([self canScrollUp]) {
+ NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
+ scrollPoint.y += deltaMenuHeight;
+ [[scrollView_ documentView] scrollPoint:scrollPoint];
+ }
+ folderFrame.size.height += deltaMenuHeight;
+ [folderView_ setFrameSize:folderFrame.size];
+ CGFloat windowWidth = [self adjustButtonWidths] + padding_;
+ NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
+ height:deltaMenuHeight];
+ CGFloat left = newWindowTopLeft.x;
+ NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
+ [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
+}
+
+// Determine window size and position.
+// Create buttons for all our nodes.
+// TODO(jrg): break up into more and smaller routines for easier unit testing.
+- (void)configureWindow {
+ const BookmarkNode* node = [parentButton_ bookmarkNode];
+ DCHECK(node);
+ int startingIndex = [[parentButton_ cell] startingChildIndex];
+ DCHECK_LE(startingIndex, node->child_count());
+ // Must have at least 1 button (for "empty")
+ int buttons = std::max(node->child_count() - startingIndex, 1);
+
+ // Prelim height of the window. We'll trim later as needed.
+ int height = [self menuHeightForButtonCount:buttons];
+ // We'll need this soon...
+ [self window];
+
+ // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
+ // http://crbug.com/35966
+ NSRect buttonsOuterFrame = NSMakeRect(
+ 0,
+ height - bookmarks::kBookmarkFolderButtonHeight -
+ bookmarks::kBookmarkVerticalPadding,
+ bookmarks::kDefaultBookmarkWidth,
+ bookmarks::kBookmarkFolderButtonHeight);
+
+ // TODO(jrg): combine with addNodesToButtonList: code from
+ // bookmark_bar_controller.mm (but use y offset)
+ // http://crbug.com/35966
+ if (node->empty()) {
+ // If no children we are the empty button.
+ BookmarkButton* button = [self makeButtonForNode:nil
+ frame:buttonsOuterFrame];
+ [buttons_ addObject:button];
+ [folderView_ addSubview:button];
+ } else {
+ for (int i = startingIndex; i < node->child_count(); ++i) {
+ const BookmarkNode* child = node->GetChild(i);
+ BookmarkButton* button = [self makeButtonForNode:child
+ frame:buttonsOuterFrame];
+ [buttons_ addObject:button];
+ [folderView_ addSubview:button];
+ buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
+ }
+ }
+ [self layOutWindowWithHeight:height];
+}
+
+- (void)layOutWindowWithHeight:(CGFloat)height {
+ // Lay out the window by adjusting all button widths to be consistent, then
+ // base the window width on this ideal button width.
+ CGFloat buttonWidth = [self adjustButtonWidths];
+ CGFloat windowWidth = buttonWidth + padding_;
+ NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
+ height:height];
+ // Make sure as much of a submenu is exposed (which otherwise would be a
+ // problem if the parent button is close to the bottom of the screen).
+ if ([parentController_ isKindOfClass:[self class]]) {
+ CGFloat minimumY = NSMinY([[[self window] screen] visibleFrame]) +
+ bookmarks::kScrollWindowVerticalMargin +
+ height;
+ newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
+ }
+ NSWindow* window = [self window];
+ NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
+ newWindowTopLeft.y - height,
+ windowWidth, height);
+ [window setFrame:windowFrame display:NO];
+ NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
+ [folderView_ setFrame:folderFrame];
+ NSSize newSize = NSMakeSize(windowWidth, 0.0);
+ [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0];
+ [self configureWindowLevel];
+ [window display];
+}
+
+// TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
+- (CGFloat)adjustButtonWidths {
+ CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
+ // Use the cell's size as the base for determining the desired width of the
+ // button rather than the button's current width. -[cell cellSize] always
+ // returns the 'optimum' size of the cell based on the cell's contents even
+ // if it's less than the current button size. Relying on the button size
+ // would result in buttons that could only get wider but we want to handle
+ // the case where the widest button gets removed from a folder menu.
+ for (BookmarkButton* button in buttons_.get())
+ width = std::max(width, [[button cell] cellSize].width);
+ width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
+ // Things look and feel more menu-like if all the buttons are the
+ // full width of the window, especially if there are submenus.
+ for (BookmarkButton* button in buttons_.get()) {
+ NSRect buttonFrame = [button frame];
+ buttonFrame.size.width = width;
+ [button setFrame:buttonFrame];
+ }
+ return width;
+}
+
+// Start a "scroll up" timer.
+- (void)beginScrollWindowUp {
+ [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
}
-- (void)setOffTheSideNodeStartIndex:(size_t)index {
- menuBridge_->set_off_the_side_node_start_index(index);
+// Start a "scroll down" timer.
+- (void)beginScrollWindowDown {
+ [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
}
-- (void)bookmarkMenuDidClose:(BookmarkMenuCocoaController*)controller {
- // Inform the bookmark bar that the folder has closed on the next iteration
- // of the event loop. If the menu was closed via a click event on a folder
- // button, this message will be received before dispatching the click event
- // to the button. If the button is the same folder button that ran the menu
- // in the first place, this will recursively pop open the menu because the
- // active folder will be nil-ed by |-closeBookmarkFolder:|. To prevent that,
- // perform the selector on the next iteration of the loop.
- [barController_ performSelector:@selector(closeBookmarkFolder:)
- withObject:self
- afterDelay:0.0];
+// End a scrolling timer. Can be called excessively with no harm.
+- (void)endScroll {
+ if (scrollTimer_) {
+ [scrollTimer_ invalidate];
+ scrollTimer_ = nil;
+ verticalScrollDelta_ = 0;
+ }
+}
+
+- (int)indexOfButton:(BookmarkButton*)button {
+ if (button == nil)
+ return -1;
+ int index = [buttons_ indexOfObject:button];
+ return (index == NSNotFound) ? -1 : index;
+}
+
+- (BookmarkButton*)buttonAtIndex:(int)which {
+ if (which < 0 || which >= [self buttonCount])
+ return nil;
+ return [buttons_ objectAtIndex:which];
+}
+
+// Private, called by performOneScroll only.
+// If the button at index contains the mouse it will select it and return YES.
+// Otherwise returns NO.
+- (BOOL)selectButtonIfHoveredAtIndex:(int)index {
+ BookmarkButton *btn = [self buttonAtIndex:index];
+ if ([[btn cell] isMouseReallyInside]) {
+ buttonThatMouseIsIn_ = btn;
+ [self setSelectedButtonByIndex:index];
+ return YES;
+ }
+ return NO;
+}
+
+// Perform a single scroll of the specified amount.
+- (void)performOneScroll:(CGFloat)delta {
+ if (delta == 0.0)
+ return;
+ CGFloat finalDelta = [self determineFinalScrollDelta:delta];
+ if (finalDelta == 0.0)
+ return;
+ int index = [self indexOfButton:buttonThatMouseIsIn_];
+ // Check for a current mouse-initiated selection.
+ BOOL maintainHoverSelection =
+ (buttonThatMouseIsIn_ &&
+ [[buttonThatMouseIsIn_ cell] isMouseReallyInside] &&
+ selectedIndex_ != -1 &&
+ index == selectedIndex_);
+ NSRect windowFrame = [[self window] frame];
+ NSSize newSize = NSMakeSize(NSWidth(windowFrame), 0.0);
+ [self adjustWindowLeft:windowFrame.origin.x
+ size:newSize
+ scrollingBy:finalDelta];
+ // We have now scrolled.
+ if (!maintainHoverSelection)
+ return;
+ // Is mouse still in the same hovered button?
+ if ([[buttonThatMouseIsIn_ cell] isMouseReallyInside])
+ return;
+ // The finalDelta scroll direction will tell us us whether to search up or
+ // down the buttons array for the newly hovered button.
+ if (finalDelta < 0.0) { // Scrolled up, so search backwards for new hover.
+ index--;
+ while (index >= 0) {
+ if ([self selectButtonIfHoveredAtIndex:index])
+ return;
+ index--;
+ }
+ } else { // Scrolled down, so search forward for new hovered button.
+ index++;
+ int btnMax = [self buttonCount];
+ while (index < btnMax) {
+ if ([self selectButtonIfHoveredAtIndex:index])
+ return;
+ index++;
+ }
+ }
+}
+
+- (CGFloat)determineFinalScrollDelta:(CGFloat)delta {
+ if ((delta > 0.0 && ![scrollUpArrowView_ isHidden]) ||
+ (delta < 0.0 && ![scrollDownArrowView_ isHidden])) {
+ NSWindow* window = [self window];
+ NSRect windowFrame = [window frame];
+ NSScreen* screen = [window screen];
+ NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
+ CGFloat scrollY = scrollPosition.y;
+ NSRect scrollerFrame = [scrollView_ frame];
+ CGFloat scrollerY = NSMinY(scrollerFrame);
+ NSRect visibleFrame = [visibleView_ frame];
+ CGFloat visibleY = NSMinY(visibleFrame);
+ CGFloat windowY = NSMinY(windowFrame);
+ CGFloat offset = scrollerY + visibleY + windowY;
+
+ if (delta > 0.0) {
+ // Scrolling up.
+ CGFloat minimumY = NSMinY([screen visibleFrame]) +
+ bookmarks::kScrollWindowVerticalMargin;
+ CGFloat maxUpDelta = scrollY - offset + minimumY;
+ delta = MIN(delta, maxUpDelta);
+ } else {
+ // Scrolling down.
+ NSRect screenFrame = [screen visibleFrame];
+ CGFloat topOfScreen = NSMaxY(screenFrame);
+ NSRect folderFrame = [folderView_ frame];
+ CGFloat folderHeight = NSHeight(folderFrame);
+ CGFloat folderTop = folderHeight - scrollY + offset;
+ CGFloat maxDownDelta =
+ topOfScreen - folderTop - bookmarks::kScrollWindowVerticalMargin;
+ delta = MAX(delta, maxDownDelta);
+ }
+ } else {
+ delta = 0.0;
+ }
+ return delta;
+}
+
+// Perform a scroll of the window on the screen.
+// Called by a timer when scrolling.
+- (void)performScroll:(NSTimer*)timer {
+ DCHECK(verticalScrollDelta_);
+ [self performOneScroll:verticalScrollDelta_];
+}
+
+
+// Add a timer to fire at a regular interval which scrolls the
+// window vertically |delta|.
+- (void)addScrollTimerWithDelta:(CGFloat)delta {
+ if (scrollTimer_ && verticalScrollDelta_ == delta)
+ return;
+ [self endScroll];
+ verticalScrollDelta_ = delta;
+ scrollTimer_ = [NSTimer timerWithTimeInterval:kBookmarkBarFolderScrollInterval
+ target:self
+ selector:@selector(performScroll:)
+ userInfo:nil
+ repeats:YES];
+
+ [[NSRunLoop mainRunLoop] addTimer:scrollTimer_ forMode:NSRunLoopCommonModes];
+}
+
+
+// Called as a result of our tracking area. Warning: on the main
+// screen (of a single-screened machine), the minimum mouse y value is
+// 1, not 0. Also, we do not get events when the mouse is above the
+// menubar (to be fixed by setting the proper window level; see
+// initializer).
+// Note [theEvent window] may not be our window, as we also get these messages
+// forwarded from BookmarkButton's mouse tracking loop.
+- (void)mouseMovedOrDragged:(NSEvent*)theEvent {
+ NSPoint eventScreenLocation =
+ [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]];
+
+ // Base hot spot calculations on the positions of the scroll arrow views.
+ NSRect testRect = [scrollDownArrowView_ frame];
+ NSPoint testPoint = [visibleView_ convertPoint:testRect.origin
+ toView:nil];
+ testPoint = [[self window] convertBaseToScreen:testPoint];
+ CGFloat closeToTopOfScreen = testPoint.y;
+
+ testRect = [scrollUpArrowView_ frame];
+ testPoint = [visibleView_ convertPoint:testRect.origin toView:nil];
+ testPoint = [[self window] convertBaseToScreen:testPoint];
+ CGFloat closeToBottomOfScreen = testPoint.y + testRect.size.height;
+ if (eventScreenLocation.y <= closeToBottomOfScreen &&
+ ![scrollUpArrowView_ isHidden]) {
+ [self beginScrollWindowUp];
+ } else if (eventScreenLocation.y > closeToTopOfScreen &&
+ ![scrollDownArrowView_ isHidden]) {
+ [self beginScrollWindowDown];
+ } else {
+ [self endScroll];
+ }
+}
+
+- (void)mouseMoved:(NSEvent*)theEvent {
+ [self mouseMovedOrDragged:theEvent];
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ [self mouseMovedOrDragged:theEvent];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ [self endScroll];
+}
+
+// Add a tracking area so we know when the mouse is pinned to the top
+// or bottom of the screen. If that happens, and if the mouse
+// position overlaps the window, scroll it.
+- (void)addOrUpdateScrollTracking {
+ [self removeScrollTracking];
+ NSView* view = [[self window] contentView];
+ scrollTrackingArea_.reset([[CrTrackingArea alloc]
+ initWithRect:[view bounds]
+ options:(NSTrackingMouseMoved |
+ NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways |
+ NSTrackingEnabledDuringMouseDrag
+ )
+ proxiedOwner:self
+ userInfo:nil]);
+ [view addTrackingArea:scrollTrackingArea_.get()];
+}
+
+// Remove the tracking area associated with scrolling.
+- (void)removeScrollTracking {
+ if (scrollTrackingArea_.get()) {
+ [[[self window] contentView] removeTrackingArea:scrollTrackingArea_.get()];
+ [scrollTrackingArea_.get() clearOwner];
+ }
+ scrollTrackingArea_.reset();
+}
+
+// Close the old hover-open bookmark folder, and open a new one. We
+// do both in one step to allow for a delay in closing the old one.
+// See comments above kDragHoverCloseDelay (bookmark_bar_controller.h)
+// for more details.
+- (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender {
+ // Ignore if sender button is in a window that's just been hidden - that
+ // would leave us with an orphaned menu. BUG 69002
+ if ([[sender window] isVisible] != YES)
+ return;
+ // If an old submenu exists, close it immediately.
+ [self closeBookmarkFolder:sender];
+
+ // Open a new one if meaningful.
+ if ([sender isFolder])
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+- (NSArray*)buttons {
+ return buttons_.get();
+}
+
+- (void)close {
+ [folderController_ close];
+ [super close];
+}
+
+- (void)scrollWheel:(NSEvent *)theEvent {
+ if (![scrollUpArrowView_ isHidden] || ![scrollDownArrowView_ isHidden]) {
+ // We go negative since an NSScrollView has a flipped coordinate frame.
+ CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY];
+ [self performOneScroll:amt];
+ }
+}
+
+#pragma mark Actions Forwarded to Parent BookmarkBarController
+
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
+ return [barController_ validateUserInterfaceItem:item];
+}
+
+- (IBAction)openBookmark:(id)sender {
+ [barController_ openBookmark:sender];
+}
+
+- (IBAction)openBookmarkInNewForegroundTab:(id)sender {
+ [barController_ openBookmarkInNewForegroundTab:sender];
+}
+
+- (IBAction)openBookmarkInNewWindow:(id)sender {
+ [barController_ openBookmarkInNewWindow:sender];
+}
+
+- (IBAction)openBookmarkInIncognitoWindow:(id)sender {
+ [barController_ openBookmarkInIncognitoWindow:sender];
+}
+
+- (IBAction)editBookmark:(id)sender {
+ [barController_ editBookmark:sender];
+}
+
+- (IBAction)cutBookmark:(id)sender {
+ [self closeBookmarkFolder:self];
+ [barController_ cutBookmark:sender];
+}
+
+- (IBAction)copyBookmark:(id)sender {
+ [barController_ copyBookmark:sender];
+}
+
+- (IBAction)pasteBookmark:(id)sender {
+ [barController_ pasteBookmark:sender];
+}
+
+- (IBAction)deleteBookmark:(id)sender {
+ [self closeBookmarkFolder:self];
+ [barController_ deleteBookmark:sender];
+}
+
+- (IBAction)openAllBookmarks:(id)sender {
+ [barController_ openAllBookmarks:sender];
+}
- // This controller is created on-demand and should be released when the menu
- // closes because a new one will be created when it is opened again.
+- (IBAction)openAllBookmarksNewWindow:(id)sender {
+ [barController_ openAllBookmarksNewWindow:sender];
+}
+
+- (IBAction)openAllBookmarksIncognitoWindow:(id)sender {
+ [barController_ openAllBookmarksIncognitoWindow:sender];
+}
+
+- (IBAction)addPage:(id)sender {
+ [barController_ addPage:sender];
+}
+
+- (IBAction)addFolder:(id)sender {
+ [barController_ addFolder:sender];
+}
+
+#pragma mark Drag & Drop
+
+// Find something like std::is_between<T>? I can't believe one doesn't exist.
+// http://crbug.com/35966
+static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
+ return ((value >= low) && (value <= high));
+}
+
+// Return the proposed drop target for a hover open button, or nil if none.
+//
+// TODO(jrg): this is just like the version in
+// bookmark_bar_controller.mm, but vertical instead of horizontal.
+// Generalize to be axis independent then share code.
+// http://crbug.com/35966
+- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
+ for (BookmarkButton* button in buttons_.get()) {
+ // No early break -- makes no assumption about button ordering.
+
+ // Intentionally NOT using NSPointInRect() so that scrolling into
+ // a submenu doesn't cause it to be closed.
+ if (ValueInRangeInclusive(NSMinY([button frame]),
+ point.y,
+ NSMaxY([button frame]))) {
+
+ // Over a button but let's be a little more specific
+ // (e.g. over the middle half).
+ NSRect frame = [button frame];
+ NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4);
+ if (ValueInRangeInclusive(NSMinY(middleHalfOfButton),
+ point.y,
+ NSMaxY(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;
+}
+
+// TODO(jrg): again we have code dup, sort of, with
+// bookmark_bar_controller.mm, but the axis is changed. One minor
+// difference is accomodation for the "empty" button (which may not
+// exist in the future).
+// http://crbug.com/35966
+- (int)indexForDragToPoint:(NSPoint)point {
+ // Identify which buttons we are between. For now, assume a button
+ // location is at the center point of its view, and that an exact
+ // match means "place before".
+ // TODO(jrg): revisit position info based on UI team feedback.
+ // dropLocation is in bar local coordinates.
+ // http://crbug.com/36276
+ NSPoint dropLocation =
+ [folderView_ convertPoint:point
+ fromView:[[self window] contentView]];
+ BookmarkButton* buttonToTheTopOfDraggedButton = nil;
+ // Buttons are laid out in this array from top to bottom (screen
+ // wise), which means "biggest y" --> "smallest y".
+ for (BookmarkButton* button in buttons_.get()) {
+ CGFloat midpoint = NSMidY([button frame]);
+ if (dropLocation.y > midpoint) {
+ break;
+ }
+ buttonToTheTopOfDraggedButton = button;
+ }
+
+ // TODO(jrg): On Windows, dropping onto (empty) highlights the
+ // entire drop location and does not use an insertion point.
+ // http://crbug.com/35967
+ if (!buttonToTheTopOfDraggedButton) {
+ // We are at the very top (we broke out of the loop on the first try).
+ return 0;
+ }
+ if ([buttonToTheTopOfDraggedButton isEmpty]) {
+ // There is a button but it's an empty placeholder.
+ // Default to inserting on top of it.
+ return 0;
+ }
+ const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton
+ bookmarkNode];
+ DCHECK(beforeNode);
+ // Be careful if the number of buttons != number of nodes.
+ return ((beforeNode->parent()->GetIndexOf(beforeNode) + 1) -
+ [[parentButton_ cell] startingChildIndex]);
+}
+
+// TODO(jrg): Yet more code dup.
+// 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]->child_count();
+ } else {
+ // Else we're dropping somewhere in the folder, so find the right spot.
+ destParent = [parentButton_ bookmarkNode];
+ destIndex = [self indexForDragToPoint:point];
+ // Be careful if the number of buttons != number of nodes.
+ destIndex += [[parentButton_ cell] startingChildIndex];
+ }
+
+ // Prevent cycles.
+ BOOL wasCopiedOrMoved = NO;
+ if (!destParent->HasAncestor(sourceNode)) {
+ if (copy)
+ [self bookmarkModel]->Copy(sourceNode, destParent, destIndex);
+ else
+ [self bookmarkModel]->Move(sourceNode, destParent, destIndex);
+ wasCopiedOrMoved = YES;
+ // Movement of a node triggers observers (like us) to rebuild the
+ // bar so we don't have to do so explicitly.
+ }
+
+ return wasCopiedOrMoved;
+}
+
+// TODO(maf): Implement live drag & drop animation using this hook.
+- (void)setDropInsertionPos:(CGFloat)where {
+}
+
+// TODO(maf): Implement live drag & drop animation using this hook.
+- (void)clearDropInsertionPos {
+}
+
+#pragma mark NSWindowDelegate Functions
+
+- (void)windowWillClose:(NSNotification*)notification {
+ // Also done by the dealloc method, but also doing it here is quicker and
+ // more reliable.
+ [parentButton_ forceButtonBorderToStayOnAlways:NO];
+
+ // If a "hover open" is pending when the bookmark bar folder is
+ // closed, be sure it gets cancelled.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+
+ [self endScroll]; // Just in case we were scrolling.
+ [barController_ childFolderWillClose:self];
+ [self closeBookmarkFolder:self];
[self autorelease];
}
-@end
+#pragma mark BookmarkButtonDelegate Protocol
+
+- (void)fillPasteboard:(NSPasteboard*)pboard
+ forDragOfButton:(BookmarkButton*)button {
+ [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
+
+ // Close our folder menu and submenus since we know we're going to be dragged.
+ [self closeBookmarkFolder:self];
+}
+
+// Called from BookmarkButton.
+// Unlike bookmark_bar_controller's version, we DO default to being enabled.
+- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
+ [[NSCursor arrowCursor] set];
+
+ buttonThatMouseIsIn_ = sender;
+ [self setSelectedButtonByIndex:[self indexOfButton:sender]];
-////////////////////////////////////////////////////////////////////////////////
+ // Cancel a previous hover if needed.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
-@implementation BookmarkBarFolderController (ExposedForTesting)
+ // If already opened, then we exited but re-entered the button
+ // (without entering another button open), do nothing.
+ if ([folderController_ parentButton] == sender)
+ return;
-- (BookmarkMenuBridge*)menuBridge {
- return menuBridge_.get();
+ [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:)
+ withObject:sender
+ afterDelay:bookmarks::kHoverOpenDelay
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
}
-@end
+// Called from the BookmarkButton
+- (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
+ if (buttonThatMouseIsIn_ == sender)
+ buttonThatMouseIsIn_ = nil;
+ [self setSelectedButtonByIndex:-1];
+
+ // Stop any timer about opening a new hover-open folder.
+
+ // Since a performSelector:withDelay: on self retains self, it is
+ // possible that a cancelPreviousPerformRequestsWithTarget: reduces
+ // the refcount to 0, releasing us. That's a bad thing to do while
+ // this object (or others it may own) is in the event chain. Thus
+ // we have a retain/autorelease.
+ [self retain];
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ [self autorelease];
+}
+
+- (NSWindow*)browserWindow {
+ return [parentController_ browserWindow];
+}
+
+- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
+ return [barController_ canEditBookmarks] &&
+ [barController_ canEditBookmark:[button bookmarkNode]];
+}
+
+- (void)didDragBookmarkToTrash:(BookmarkButton*)button {
+ [barController_ didDragBookmarkToTrash:button];
+}
+
+- (void)bookmarkDragDidEnd:(BookmarkButton*)button
+ operation:(NSDragOperation)operation {
+ [barController_ bookmarkDragDidEnd:button
+ operation:operation];
+}
+
+
+#pragma mark BookmarkButtonControllerProtocol
+
+// Recursively close all bookmark folders.
+- (void)closeAllBookmarkFolders {
+ // Closing the top level implicitly closes all children.
+ [barController_ closeAllBookmarkFolders];
+}
+
+// Close our bookmark folder (a sub-controller) if we have one.
+- (void)closeBookmarkFolder:(id)sender {
+ if (folderController_) {
+ // Make this menu key, so key status doesn't go back to the browser
+ // window when the submenu closes.
+ [[self window] makeKeyWindow];
+ [self setSubFolderGrowthToRight:YES];
+ [[folderController_ window] close];
+ folderController_ = nil;
+ }
+}
+
+- (BookmarkModel*)bookmarkModel {
+ return [barController_ bookmarkModel];
+}
+
+- (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
+ return [barController_ draggingAllowed:info];
+}
+
+// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
+// Most of the work (e.g. drop indicator) is taken care of in the
+// folder_view. Here we handle hover open issues for subfolders.
+// Caution: there are subtle differences between this one and
+// bookmark_bar_controller.mm's version.
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
+ NSPoint currentLocation = [info draggingLocation];
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
+
+ // 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;
+ }
+ }
+ // Delegate handling of dragging over a button to the |hoverState_| member.
+ return [hoverState_ draggingEnteredButton:button];
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info {
+ return NSDragOperationMove;
+}
+
+// Unlike bookmark_bar_controller, we need to keep track of dragging state.
+// We also need to make sure we cancel the delayed hover close.
+- (void)draggingExited:(id<NSDraggingInfo>)info {
+ // NOT the same as a cancel --> we may have moved the mouse into the submenu.
+ // Delegate handling of the hover button to the |hoverState_| member.
+ [hoverState_ draggingExited];
+}
+
+- (BOOL)dragShouldLockBarVisibility {
+ return [parentController_ dragShouldLockBarVisibility];
+}
+
+// TODO(jrg): ARGH more code dup.
+// 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];
+}
+
+// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
+// http://crbug.com/35966
+- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
+ BOOL dragged = NO;
+ std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
+ 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;
+}
+
+// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
+// http://crbug.com/35966
+- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
+ std::vector<const BookmarkNode*> dragDataNodes;
+ BookmarkNodeData 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.
+// TODO(jrg): ARGH code dup!
+// http://crbug.com/35966
+- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
+ return ![self buttonForDroppingOnAtPoint:point];
+}
+
+// Button selection change code to support type to select and arrow key events.
+#pragma mark Keyboard Support
+
+// Scroll the menu to show the selected button, if it's not already visible.
+- (void)showSelectedButton {
+ int bMaxIndex = [self buttonCount] - 1; // Max array index in button array.
+
+ // Is there a valid selected button?
+ if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex)
+ return;
+
+ // Is the menu scrollable anyway?
+ if (![self canScrollUp] && ![self canScrollDown])
+ return;
+
+ // Now check to see if we need to scroll, which way, and how far.
+ CGFloat delta = 0.0;
+ NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
+ CGFloat itemBottom = (bMaxIndex - selectedIndex_) *
+ bookmarks::kBookmarkFolderButtonHeight;
+ CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight;
+ CGFloat viewHeight = NSHeight([scrollView_ frame]);
+
+ if (scrollPoint.y > itemBottom) { // Need to scroll down.
+ delta = scrollPoint.y - itemBottom;
+ } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up.
+ delta = -(itemTop - (scrollPoint.y + viewHeight));
+ } else { // No need to scroll.
+ return;
+ }
+
+ [self performOneScroll:delta];
+}
+
+// All changes to selectedness of buttons (aka fake menu items) ends up
+// calling this method to actually flip the state of items.
+// Needs to handle -1 as the invalid index (when nothing is selected) and
+// greater than range values too.
+- (void)setStateOfButtonByIndex:(int)index
+ state:(bool)state {
+ if (index >= 0 && index < [self buttonCount])
+ [[buttons_ objectAtIndex:index] highlight:state];
+}
+
+// Selects the required button and deselects the previously selected one.
+// An index of -1 means no selection.
+- (void)setSelectedButtonByIndex:(int)index {
+ if (index == selectedIndex_)
+ return;
+
+ [self setStateOfButtonByIndex:selectedIndex_ state:NO];
+ [self setStateOfButtonByIndex:index state:YES];
+ selectedIndex_ = index;
+
+ [self showSelectedButton];
+}
+
+- (void)clearInputText {
+ [typedPrefix_ release];
+ typedPrefix_ = nil;
+}
+
+// Find the earliest item in the folder which has the target prefix.
+// Returns nil if there is no prefix or there are no matches.
+// These are in no particular order, and not particularly numerous, so linear
+// search should be OK.
+// -1 means no match.
+- (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix {
+ if ([prefix length] == 0) // Also handles nil.
+ return -1;
+ int maxButtons = [buttons_ count];
+ NSString *lowercasePrefix = [prefix lowercaseString];
+ for (int i = 0 ; i < maxButtons ; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix])
+ return i;
+ }
+ return -1;
+}
+
+- (void)setSelectedButtonByPrefix:(NSString*)prefix {
+ [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]];
+}
+
+- (void)selectPrevious {
+ int newIndex;
+ if (selectedIndex_ == 0)
+ return;
+ if (selectedIndex_ < 0)
+ newIndex = [self buttonCount] -1;
+ else
+ newIndex = std::max(selectedIndex_ - 1, 0);
+ [self setSelectedButtonByIndex:newIndex];
+}
+
+- (void) selectNext {
+ if (selectedIndex_ + 1 < [self buttonCount])
+ [self setSelectedButtonByIndex:selectedIndex_ + 1];
+}
+
+- (BOOL)handleInputText:(NSString*)newText {
+ const unichar kUnicodeEscape = 0x001B;
+ const unichar kUnicodeSpace = 0x0020;
+
+ // Event goes to the deepest nested open submenu.
+ if (folderController_)
+ return [folderController_ handleInputText:newText];
+
+ // Look for arrow keys or other function keys.
+ if ([newText length] == 1) {
+ // Get the 16-bit unicode char.
+ unichar theChar = [newText characterAtIndex:0];
+ switch (theChar) {
+
+ // Keys that trigger opening of the selection.
+ case kUnicodeSpace: // Space.
+ case NSNewlineCharacter:
+ case NSCarriageReturnCharacter:
+ case NSEnterCharacter:
+ if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) {
+ [self openBookmark:[buttons_ objectAtIndex:selectedIndex_]];
+ return NO; // NO because the selection-handling code will close later.
+ } else {
+ return YES; // Triggering with no selection closes the menu.
+ }
+ // Keys that cancel and close the menu.
+ case kUnicodeEscape:
+ case NSDeleteCharacter:
+ case NSBackspaceCharacter:
+ [self clearInputText];
+ return YES;
+ // Keys that change selection directionally.
+ case NSUpArrowFunctionKey:
+ [self clearInputText];
+ [self selectPrevious];
+ return NO;
+ case NSDownArrowFunctionKey:
+ [self clearInputText];
+ [self selectNext];
+ return NO;
+ // Keys that open and close submenus.
+ case NSRightArrowFunctionKey: {
+ BookmarkButton* btn = [self buttonAtIndex:selectedIndex_];
+ if (btn && [btn isFolder]) {
+ [self openBookmarkFolderFromButtonAndCloseOldOne:btn];
+ [folderController_ selectNext];
+ }
+ [self clearInputText];
+ return NO;
+ }
+ case NSLeftArrowFunctionKey:
+ [self clearInputText];
+ [parentController_ closeBookmarkFolder:self];
+ return NO;
+
+ // Check for other keys that should close the menu.
+ default: {
+ if (theChar > NSUpArrowFunctionKey &&
+ theChar <= NSModeSwitchFunctionKey) {
+ [self clearInputText];
+ return YES;
+ }
+ break;
+ }
+ }
+ }
+
+ // It is a char or string worth adding to the type-select buffer.
+ NSString *newString = (!typedPrefix_) ?
+ newText : [typedPrefix_ stringByAppendingString:newText];
+ [typedPrefix_ release];
+ typedPrefix_ = [newString retain];
+ [self setSelectedButtonByPrefix:typedPrefix_];
+ return NO;
+}
+
+// Return the y position for a drop indicator.
+//
+// TODO(jrg): again we have code dup, sort of, with
+// bookmark_bar_controller.mm, but the axis is changed.
+// http://crbug.com/35966
+- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
+ CGFloat y = 0;
+ int destIndex = [self indexForDragToPoint:point];
+ int numButtons = static_cast<int>([buttons_ count]);
+
+ // If it's a drop strictly between existing buttons or at the very beginning
+ 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];
+ y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding;
+
+ // 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 below it.
+ BookmarkButton* button =
+ [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
+ DCHECK(button);
+ NSRect buttonFrame = [button frame];
+ y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding;
+
+ }
+ } else {
+ NOTREACHED();
+ }
+
+ return y;
+}
+
+- (ui::ThemeProvider*)themeProvider {
+ return [parentController_ themeProvider];
+}
+
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
+ // Do nothing.
+}
+
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
+ // Do nothing.
+}
+
+- (BookmarkBarFolderController*)folderController {
+ return folderController_;
+}
+
+- (void)faviconLoadedForNode:(const BookmarkNode*)node {
+ for (BookmarkButton* button in buttons_.get()) {
+ if ([button bookmarkNode] == node) {
+ [button setImage:[barController_ faviconForNode:node]];
+ [button setNeedsDisplay:YES];
+ return;
+ }
+ }
+
+ // Node was not in this menu, try submenu.
+ if (folderController_)
+ [folderController_ faviconLoadedForNode:node];
+}
+
+// Add a new folder controller as triggered by the given folder button.
+- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
+ if (folderController_)
+ [self closeBookmarkFolder:self];
+
+ // Folder controller, like many window controllers, owns itself.
+ folderController_ =
+ [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
+ parentController:self
+ barController:barController_];
+ [folderController_ showWindow:self];
+}
+
+- (void)openAll:(const BookmarkNode*)node
+ disposition:(WindowOpenDisposition)disposition {
+ [barController_ openAll:node disposition:disposition];
+}
+
+- (void)addButtonForNode:(const BookmarkNode*)node
+ atIndex:(NSInteger)buttonIndex {
+ // Propose the frame for the new button. By default, this will be set to the
+ // topmost button's frame (and there will always be one) offset upward in
+ // anticipation of insertion.
+ NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
+ newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
+ // When adding a button to an empty folder we must remove the 'empty'
+ // placeholder button. This can be detected by checking for a parent
+ // child count of 1.
+ const BookmarkNode* parentNode = node->parent();
+ if (parentNode->child_count() == 1) {
+ BookmarkButton* emptyButton = [buttons_ lastObject];
+ newButtonFrame = [emptyButton frame];
+ [emptyButton setDelegate:nil];
+ [emptyButton removeFromSuperview];
+ [buttons_ removeLastObject];
+ }
+
+ if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
+ buttonIndex = [buttons_ count];
+
+ // Offset upward by one button height all buttons above insertion location.
+ BookmarkButton* button = nil; // Remember so it can be de-highlighted.
+ for (NSInteger i = 0; i < buttonIndex; ++i) {
+ button = [buttons_ objectAtIndex:i];
+ // Remember this location in case it's the last button being moved
+ // which is where the new button will be located.
+ newButtonFrame = [button frame];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
+ [button setFrame:buttonFrame];
+ }
+ [[button cell] mouseExited:nil]; // De-highlight.
+ BookmarkButton* newButton = [self makeButtonForNode:node
+ frame:newButtonFrame];
+ [buttons_ insertObject:newButton atIndex:buttonIndex];
+ [folderView_ addSubview:newButton];
+
+ // Close any child folder(s) which may still be open.
+ [self closeBookmarkFolder:self];
+
+ [self adjustWindowForButtonCount:[buttons_ count]];
+}
+
+// More code which essentially duplicates that of BookmarkBarController.
+// TODO(mrossetti,jrg): 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];
+ BookmarkModel* bookmarkModel = [self bookmarkModel];
+ const BookmarkNode* destParent = NULL;
+ int destIndex = 0;
+ if ([button isFolder]) {
+ destParent = [button bookmarkNode];
+ // Drop it at the end.
+ destIndex = [button bookmarkNode]->child_count();
+ } else {
+ // Else we're dropping somewhere in the folder, so find the right spot.
+ destParent = [parentButton_ bookmarkNode];
+ destIndex = [self indexForDragToPoint:point];
+ // Be careful if the number of buttons != number of nodes.
+ destIndex += [[parentButton_ cell] startingChildIndex];
+ }
+
+ // 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::SysNSStringToUTF16([titles objectAtIndex:i]),
+ gurl);
+ nodesWereAdded = YES;
+ }
+ }
+ return nodesWereAdded;
+}
+
+- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
+ if (fromIndex != toIndex) {
+ if (toIndex == -1)
+ toIndex = [buttons_ count];
+ BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
+ if (movedButton == buttonThatMouseIsIn_)
+ buttonThatMouseIsIn_ = nil;
+ [buttons_ removeObjectAtIndex:fromIndex];
+ NSRect movedFrame = [movedButton frame];
+ NSPoint toOrigin = movedFrame.origin;
+ [movedButton setHidden:YES];
+ if (fromIndex < toIndex) {
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
+ toOrigin = [targetButton frame].origin;
+ for (NSInteger i = fromIndex; i < toIndex; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect frame = [button frame];
+ frame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
+ [button setFrameOrigin:frame.origin];
+ }
+ } else {
+ 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.y -= bookmarks::kBookmarkFolderButtonHeight;
+ [button setFrameOrigin:buttonFrame.origin];
+ }
+ }
+ [buttons_ insertObject:movedButton atIndex:toIndex];
+ [movedButton setFrameOrigin:toOrigin];
+ [movedButton setHidden:NO];
+ }
+}
+
+// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
+- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
+ // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
+ BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
+ NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
+
+ // If a hover-open is pending, cancel it.
+ if (oldButton == buttonThatMouseIsIn_) {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ buttonThatMouseIsIn_ = nil;
+ }
+
+ // Deleting a button causes rearrangement that enables us to lose a
+ // mouse-exited event. This problem doesn't appear to exist with
+ // other keep-menu-open options (e.g. add folder). Since the
+ // showsBorderOnlyWhileMouseInside uses a tracking area, simple
+ // tricks (e.g. sending an extra mouseExited: to the button) don't
+ // fix the problem.
+ // http://crbug.com/54324
+ for (NSButton* button in buttons_.get()) {
+ if ([button showsBorderOnlyWhileMouseInside]) {
+ [button setShowsBorderOnlyWhileMouseInside:NO];
+ [button setShowsBorderOnlyWhileMouseInside:YES];
+ }
+ }
+
+ [oldButton setDelegate:nil];
+ [oldButton removeFromSuperview];
+ [buttons_ removeObjectAtIndex:buttonIndex];
+ for (NSInteger i = 0; i < buttonIndex; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
+ [button setFrame:buttonFrame];
+ }
+ // Search for and adjust submenus, if necessary.
+ NSInteger buttonCount = [buttons_ count];
+ if (buttonCount) {
+ BookmarkButton* subButton = [folderController_ parentButton];
+ for (NSButton* aButton in buttons_.get()) {
+ // If this button is showing its menu then we need to move the menu, too.
+ if (aButton == subButton)
+ [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0,
+ bookmarks::kBookmarkBarHeight)];
+ }
+ } else {
+ // If all nodes have been removed from this folder then add in the
+ // 'empty' placeholder button.
+ NSRect buttonFrame =
+ NSMakeRect(0.0, 0.0, bookmarks::kDefaultBookmarkWidth,
+ bookmarks::kBookmarkFolderButtonHeight);
+ BookmarkButton* button = [self makeButtonForNode:nil
+ frame:buttonFrame];
+ [buttons_ addObject:button];
+ [folderView_ addSubview:button];
+ buttonCount = 1;
+ }
+
+ [self adjustWindowForButtonCount:buttonCount];
+
+ if (animate && !ignoreAnimations_)
+ NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
+ NSZeroSize, nil, nil, nil);
+}
+
+- (id<BookmarkButtonControllerProtocol>)controllerForNode:
+ (const BookmarkNode*)node {
+ // See if we are holding this node, otherwise see if it is in our
+ // hierarchy of visible folder menus.
+ if ([parentButton_ bookmarkNode] == node)
+ return self;
+ return [folderController_ controllerForNode:node];
+}
+
+#pragma mark TestingAPI Only
+
+- (BOOL)canScrollUp {
+ return ![scrollUpArrowView_ isHidden];
+}
+
+- (BOOL)canScrollDown {
+ return ![scrollDownArrowView_ isHidden];
+}
+
+- (CGFloat)verticalScrollArrowHeight {
+ return verticalScrollArrowHeight_;
+}
+
+- (NSView*)visibleView {
+ return visibleView_;
+}
+
+- (NSScrollView*)scrollView {
+ return scrollView_;
+}
+
+- (NSView*)folderView {
+ return folderView_;
+}
+
+- (void)setIgnoreAnimations:(BOOL)ignore {
+ ignoreAnimations_ = ignore;
+}
+
+- (BookmarkButton*)buttonThatMouseIsIn {
+ return buttonThatMouseIsIn_;
+}
+
+@end // BookmarkBarFolderController

Powered by Google App Engine
This is Rietveld 408576698