| // Copyright (c) 2012 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 <stddef.h> |
| |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/sdk_forward_declarations.h" |
| #include "base/strings/sys_string_conversions.h" |
| #import "chrome/browser/bookmarks/bookmark_model_factory.h" |
| #import "chrome/browser/bookmarks/managed_bookmark_service_factory.h" |
| #import "chrome/browser/profiles/profile.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/bookmarks/bookmark_menu_cocoa_controller.h" |
| #import "chrome/browser/ui/cocoa/browser_window_controller.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/bookmarks/browser/bookmark_node_data.h" |
| #import "components/bookmarks/managed/managed_bookmark_service.h" |
| #include "grit/ui_resources.h" |
| #include "ui/base/clipboard/clipboard_util_mac.h" |
| #include "ui/base/cocoa/cocoa_base_utils.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/theme_provider.h" |
| |
| using bookmarks::BookmarkModel; |
| using bookmarks::BookmarkNode; |
| using bookmarks::BookmarkNodeData; |
| 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). |
| |
| // Bottom of the screen's available area (excluding dock height and padding). |
| CGFloat minimumY; |
| // Bottom of the screen. |
| CGFloat screenBottomY; |
| 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), |
| minimumY(0.0), |
| screenBottomY(0.0), |
| oldWindowY(0.0), |
| folderY(0.0), |
| folderTop(0.0) {} |
| }; |
| |
| NSRect GetFirstButtonFrameForHeight(CGFloat height) { |
| CGFloat y = height - bookmarks::kBookmarkFolderButtonHeight - |
| bookmarks::kBookmarkVerticalPadding; |
| return NSMakeRect(0, y, bookmarks::kDefaultBookmarkWidth, |
| bookmarks::kBookmarkFolderButtonHeight); |
| } |
| |
| } // namespace |
| |
| |
| // Required to set the right tracking bounds for our fake menus. |
| @interface NSView(Private) |
| - (void)_updateTrackingAreas; |
| @end |
| |
| @interface BookmarkBarFolderController () |
| - (void)configureWindow; |
| - (void)addOrUpdateScrollTracking; |
| - (void)removeScrollTracking; |
| - (void)endScroll; |
| - (void)addScrollTimerWithDelta:(CGFloat)delta; |
| |
| // Return the screen to which the menu should be restricted. The screen list is |
| // very volatile and can change with very short notice so it isn't worth |
| // caching. http://crbug.com/463458 |
| - (NSScreen*)menuScreen; |
| |
| // 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; |
| |
| @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 |
| profile:(Profile*)profile { |
| NSString* nibPath = |
| [base::mac::FrameworkBundle() pathForResource:@"BookmarkBarFolderWindow" |
| ofType:@"nib"]; |
| if ((self = [super initWithWindowNibPath:nibPath owner:self])) { |
| parentButton_.reset([button retain]); |
| selectedIndex_ = -1; |
| |
| profile_ = profile; |
| |
| // 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 profile:profile]); |
| [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]); |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| NSImage* image = rb.GetNativeImageNamed(IDR_MENU_OVERFLOW_DOWN).ToNSImage(); |
| [[scrollUpArrowView_.subviews objectAtIndex:0] setImage:image]; |
| |
| image = rb.GetNativeImageNamed(IDR_MENU_OVERFLOW_UP).ToNSImage(); |
| [[scrollDownArrowView_.subviews objectAtIndex:0] setImage:image]; |
| } |
| |
| // 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; |
| BookmarkContextMenuCocoaController* menuController = |
| [barController_ menuController]; |
| BookmarkBarFolderButtonCell* cell = |
| [BookmarkBarFolderButtonCell buttonCellForNode:child |
| text:nil |
| image:image |
| menuController:menuController]; |
| [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:barController_]; |
| [button setAction:@selector(openBookmark:)]; |
| [button setAcceptsTrackIn:YES]; |
| } |
| // Add a tooltip. |
| [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]]; |
| } 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. |
| NSRect screenVisibleFrame = [[self menuScreen] visibleFrame]; |
| 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(screenVisibleFrame)) { |
| [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(screenVisibleFrame)) { |
| [self setSubFolderGrowthToRight:YES]; |
| } else { |
| return x; |
| } |
| } |
| } |
| // Unhappy; do the best we can. |
| return NSMaxX(screenVisibleFrame) - 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 = ui::ConvertPointFromWindowToScreen( |
| [parentButton_ window], |
| [parentButton_ convertPoint:NSZeroPoint toView:nil]); |
| NSPoint bookmarkBarBottomLeftInScreen = ui::ConvertPointFromWindowToScreen( |
| [parentButton_ window], |
| [[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 or right. It is |
| // intentional that top level folders "push left" or "push right" slightly |
| // different than subfolders. |
| NSRect screenVisibleFrame = [[self menuScreen] visibleFrame]; |
| // Test if window goes off-screen on the right side. |
| CGFloat spillOff = |
| newWindowTopLeft.x + windowWidth - NSMaxX(screenVisibleFrame); |
| if (spillOff > 0.0) { |
| newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff, |
| NSMinX(screenVisibleFrame)); |
| } else if (newWindowTopLeft.x < NSMinX(screenVisibleFrame)) { |
| // For left side. |
| newWindowTopLeft.x = NSMinX(screenVisibleFrame); |
| } |
| // 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. |
| CGFloat availableVerticalSpace = newWindowTopLeft.y - |
| (NSMinY(screenVisibleFrame) + bookmarks::kScrollWindowVerticalMargin); |
| if ((availableVerticalSpace < kMinSqueezedMenuHeight) && |
| (windowHeight > availableVerticalSpace)) { |
| newWindowTopLeft.y = std::min( |
| newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]), |
| NSMaxY(screenVisibleFrame)); |
| } |
| } 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 = ui::ConvertPointFromWindowToScreen( |
| [parentButton_ window], |
| [[parentButton_ superview] convertPoint:topOfWindow toView:nil]); |
| newWindowTopLeft.y = topOfWindow.y + |
| 2 * bookmarks::kBookmarkVerticalPadding; |
| } |
| 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([[self menuScreen] visibleFrame]) + |
| bookmarks::kScrollWindowVerticalMargin; |
| metrics.screenBottomY = NSMinY([[self menuScreen] frame]); |
| 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); |
| CGFloat effectiveFolderY = metrics.folderY; |
| if (!metrics.couldScrollUp && !metrics.couldScrollDown) |
| effectiveFolderY -= metrics.windowSize.height; |
| metrics.canScrollUp = effectiveFolderY < metrics.minimumY; |
| CGFloat maximumY = |
| NSMaxY([[self menuScreen] 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.screenBottomY - metrics.oldWindowY; |
| metrics.deltaWindowHeight = -metrics.deltaWindowY; |
| metrics.deltaVisibleY = metrics.minimumY - metrics.screenBottomY; |
| 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.screenBottomY + |
| 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 |
| const CGFloat maximumY = NSMaxY([[self menuScreen] visibleFrame]); |
| metrics.deltaWindowHeight += (maximumY - 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:buttonCount]; |
| 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 = GetFirstButtonFrameForHeight(height); |
| |
| // 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 menuScreen] 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]; |
| |
| // For some reason, when opening a "large" bookmark folder (containing 12 or |
| // more items) using the keyboard, the scroll view seems to want to be |
| // offset by default: [ http://crbug.com/101099 ]. Explicitly reseting the |
| // scroll position here is a bit hacky, but it does seem to work. |
| [[scrollView_ contentView] scrollToPoint:NSZeroPoint]; |
| |
| 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; |
| NSInteger 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* button = [self buttonAtIndex:index]; |
| if ([[button cell] isMouseReallyInside]) { |
| buttonThatMouseIsIn_ = button; |
| [self setSelectedButtonByIndex:index]; |
| return YES; |
| } |
| return NO; |
| } |
| |
| // Perform a single scroll of the specified amount. If |updateMouseSection| is |
| // YES, and the mouse cursor is over the currently selected item, then change |
| // the selection to the item under the mouse cursor after the scroll. |
| - (void)performOneScroll:(CGFloat)delta |
| updateMouseSelection:(BOOL)updateMouseSelection { |
| 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 = |
| (updateMouseSelection && |
| buttonThatMouseIsIn_ && |
| [[buttonThatMouseIsIn_ cell] isMouseReallyInside] && |
| selectedIndex_ != -1 && |
| index == selectedIndex_); |
| NSRect windowFrame = [[self window] frame]; |
| NSSize newSize = NSMakeSize(NSWidth(windowFrame), 0.0); |
| isScrolling_ = YES; |
| [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]; |
| NSRect screenVisibleFrame = [[self menuScreen] visibleFrame]; |
| 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(screenVisibleFrame) + |
| bookmarks::kScrollWindowVerticalMargin; |
| CGFloat maxUpDelta = scrollY - offset + minimumY; |
| delta = MIN(delta, maxUpDelta); |
| } else { |
| // Scrolling down. |
| CGFloat topOfScreen = NSMaxY(screenVisibleFrame); |
| 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_); |
| // Since this scroll was initiated by hovering over an arrow, there should be |
| // no mouse selection to update. |
| [self performOneScroll:verticalScrollDelta_ updateMouseSelection:NO]; |
| } |
| |
| |
| // 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]; |
| } |
| |
| - (NSScreen*)menuScreen { |
| // Return the parent button's screen for use as the screen upon which all |
| // display happens. This loop over all screens is not equivalent to |
| // |[[button window] screen]|. BookmarkButtons are commonly positioned near |
| // the edge of their windows (both in the bookmark bar and in other bookmark |
| // menus), and |[[button window] screen]| would return the screen that the |
| // majority of their window was on even if the parent button were clearly |
| // contained within a different screen. |
| NSButton* button = parentButton_.get(); |
| NSRect parentButtonGlobalFrame = |
| [button convertRect:[button bounds] toView:nil]; |
| parentButtonGlobalFrame = |
| [[button window] convertRectToScreen:parentButtonGlobalFrame]; |
| for (NSScreen* screen in [NSScreen screens]) { |
| if (NSIntersectsRect([screen frame], parentButtonGlobalFrame)) |
| return screen; |
| } |
| |
| // The parent button is offscreen. The ideal thing to do would be to calculate |
| // the "closest" screen, the screen which has an edge parallel to, and the |
| // least distance from, one of the edges of the button. However, popping a |
| // subfolder from an offscreen button is an unrealistic edge case and so this |
| // ideal remains unrealized. Cheat instead; this code is wrong but a lot |
| // simpler. |
| return [[button window] screen]; |
| } |
| |
| // 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 = ui::ConvertPointFromWindowToScreen( |
| [theEvent window], [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 = ui::ConvertPointFromWindowToScreen([self window], testPoint); |
| CGFloat closeToTopOfScreen = testPoint.y; |
| |
| testRect = [scrollUpArrowView_ frame]; |
| testPoint = [visibleView_ convertPoint:testRect.origin toView:nil]; |
| testPoint = ui::ConvertPointFromWindowToScreen([self window], 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 |
| ) |
| owner: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]; |
| // Make sure that the selection stays under the mouse for scroll wheel |
| // scrolls. |
| [self performOneScroll:amt updateMouseSelection:YES]; |
| } |
| } |
| |
| #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 { |
| NSPoint localPoint = [folderView_ convertPoint:point fromView:nil]; |
| 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]), |
| localPoint.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), |
| localPoint.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]; |
| } |
| |
| bookmarks::ManagedBookmarkService* managed = |
| ManagedBookmarkServiceFactory::GetForProfile(profile_); |
| if (!managed->CanBeEditedByUser(destParent)) |
| return NO; |
| if (!managed->CanBeEditedByUser(sourceNode)) |
| copy = YES; |
| |
| // 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 |
| |
| - (NSPasteboardItem*)pasteboardItemForDragOfButton:(BookmarkButton*)button { |
| return [[self folderTarget] pasteboardItemForDragOfButton:button]; |
| } |
| |
| - (void)willBeginPasteboardDrag { |
| // 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 { |
| // Prevent unnecessary button selection change while scrolling due to the |
| // size changing that happens in -performOneScroll:. |
| if (isScrolling_) |
| return; |
| |
| [[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; |
| |
| // If right click was done immediately on entering a button, then open the |
| // folder without delay so that context menu appears over the folder menu. |
| if ([event type] == NSRightMouseDown) |
| [self openBookmarkFolderFromButtonAndCloseOldOne:sender]; |
| else |
| [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]; |
| |
| // During scrolling -mouseExitedButton: stops scrolling, so update the |
| // corresponding status field to reflect is has stopped. |
| isScrolling_ = NO; |
| |
| // 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 [barController_ 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:ui::ClipboardUtil::UTIForPasteboardType( |
| 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.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) { |
| BookmarkModel* bookmarkModel = [self bookmarkModel]; |
| std::vector<const BookmarkNode*> nodes( |
| dragData.GetNodes(bookmarkModel, profile_->GetPath())); |
| 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; |
| } |
| |
| // We have just updated the selection and are about to scroll it to be |
| // visible; don't change the selection based on the mouse cursor location. |
| [self performOneScroll:delta updateMouseSelection:NO]; |
| } |
| |
| // 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]) { |
| [barController_ 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; |
| } |
| |
| - (Profile*)profile { |
| return profile_; |
| } |
| |
| - (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_ |
| profile:profile_]; |
| [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]; |
| } |
| |
| bookmarks::ManagedBookmarkService* managed = |
| ManagedBookmarkServiceFactory::GetForProfile(profile_); |
| if (!managed->CanBeEditedByUser(destParent)) |
| return NO; |
| |
| // 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 this button has an open sub-folder, close it. |
| if ([folderController_ parentButton] == oldButton) |
| [self closeBookmarkFolder:self]; |
| |
| // 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]; |
| NSInteger targetIndex = 0; |
| for (NSButton* aButton in buttons_.get()) { |
| targetIndex++; |
| // If this button is showing its menu and is below the removed button, |
| // i.e its index is greater, then we need to move the menu too. |
| if (aButton == subButton && targetIndex > buttonIndex) { |
| [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0, |
| bookmarks::kBookmarkFolderButtonHeight)]; |
| break; |
| } |
| } |
| } else if (parentButton_ != [barController_ otherBookmarksButton]) { |
| // If all nodes have been removed from this folder then add in the |
| // 'empty' placeholder button except for "Other bookmarks" folder |
| // as we are going to hide it. |
| NSRect buttonFrame = |
| GetFirstButtonFrameForHeight([self menuHeightForButtonCount:1]); |
| BookmarkButton* button = [self makeButtonForNode:nil |
| frame:buttonFrame]; |
| [buttons_ addObject:button]; |
| [folderView_ addSubview:button]; |
| buttonCount = 1; |
| } |
| |
| // buttonCount will be 0 if "Other bookmarks" folder is empty, so close |
| // the folder before hiding it. |
| if (buttonCount == 0) |
| [barController_ closeBookmarkFolder:nil]; |
| else if (buttonCount > 0) |
| [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]; |
| } |
| |
| - (BOOL)isScrolling { |
| return isScrolling_; |
| } |
| |
| - (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 |