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 |