// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/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_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/browser_window_controller.h" #import "chrome/browser/ui/cocoa/event_utils.h" #include "ui/base/theme_provider.h" 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. |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; @end @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 parentController:(BookmarkBarFolderController*)parentController barController:(BookmarkBarController*)barController { NSString* nibPath = [base::mac::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow" ofType:@"nib"]; if ((self = [super initWithWindowNibPath:nibPath owner:self])) { parentButton_.reset([button retain]); 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)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. NSString* title = base::SysUTF16ToNSString(node->GetTitle()); std::string urlString = node->GetURL().possibly_invalid_spec(); NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, urlString.c_str()]; [button setToolTip:tooltip]; [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; } } } - (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 { 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)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->child_count()) { // 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]; } // Start a "scroll down" timer. - (void)beginScrollWindowDown { [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount]; } // 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 { // 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)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]; } - (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? 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]; } #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]; // If already opened, then we exited but re-entered the button // (without entering another button open), do nothing. if ([folderController_ parentButton] == sender) return; [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:) withObject:sender afterDelay:bookmarks::kHoverOpenDelay inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; } // 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)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)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)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)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)info { BOOL dragged = NO; std::vector nodes([self retrieveBookmarkNodeData]); if (nodes.size()) { BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); NSPoint dropPoint = [info draggingLocation]; for (std::vector::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)retrieveBookmarkNodeData { std::vector dragDataNodes; BookmarkNodeData dragData; if(dragData.ReadFromDragClipboard()) { BookmarkModel* bookmarkModel = [self bookmarkModel]; Profile* profile = bookmarkModel->profile(); std::vector 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([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(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(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)child { // Do nothing. } - (void)childFolderWillClose:(id)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 (NSInteger i = buttonIndex; i < buttonCount; ++i) { BookmarkButton* aButton = [buttons_ objectAtIndex:i]; // 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)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