blob: 7cdba41c04bdfd535006fc1ae89d4a6b8ec9b407 [file] [log] [blame]
// 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