<commit_before>// 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 "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/event_utils.h"
#include "ui/base/theme_provider.h"

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) {}
};

}  // namespace


// Required to set the right tracking bounds for our fake menus.
@interface NSView(Private)
- (void)_updateTrackingAreas;
@end

@interface BookmarkBarFolderController(Private)
- (void)configureWindow;
- (void)addOrUpdateScrollTracking;
- (void)removeScrollTracking;
- (void)endScroll;
- (void)addScrollTimerWithDelta:(CGFloat)delta;

// Helper function to configureWindow which performs a basic layout of
// the window subviews, in particular the menu buttons and the window width.
- (void)layOutWindowWithHeight:(CGFloat)height;

// Determine the best button width (which will be the widest button or the
// maximum allowable button width, whichever is less) and resize all buttons.
// Return the new width so that the window can be adjusted.
- (CGFloat)adjustButtonWidths;

// Returns the total menu height needed to display |buttonCount| buttons.
// Does not do any fancy tricks like trimming the height to fit on the screen.
- (int)menuHeightForButtonCount:(int)buttonCount;

// Adjust layout of the folder menu window components, showing/hiding the
// scroll up/down arrows, and resizing as necessary for a proper disaplay.
// In order to reduce window flicker, all layout changes are deferred until
// the final step of the adjustment. To accommodate this deferral, window
// height and width changes needed by callers to this function pass their
// desired window changes in |size|. When scrolling is to be performed
// any scrolling change is given by |scrollDelta|. The ultimate amount of
// scrolling may be different from |scrollDelta| in order to accommodate
// changes in the scroller view layout. These proposed window adjustments
// are passed to helper functions using a LayoutMetrics structure.
//
// This function should be called when: 1) initially setting up a folder menu
// window, 2) responding to scrolling of the contents (which may affect the
// height of the window), 3) addition or removal of bookmark items (such as
// during cut/paste/delete/drag/drop operations).
- (void)adjustWindowLeft:(CGFloat)windowLeft
                    size:(NSSize)windowSize
             scrollingBy:(CGFloat)scrollDelta;

// Support function for adjustWindowLeft:size:scrollingBy: which initializes
// the layout adjustments by gathering current folder menu window and subviews
// positions and sizes. This information is set in the |layoutMetrics|
// structure.
- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics;

// Support function for adjustWindowLeft:size:scrollingBy: which calculates
// the changes which must be applied to the folder menu window and subviews
// positions and sizes. |layoutMetrics| contains the proposed window size
// and scrolling along with the other current window and subview layout
// information. The values in |layoutMetrics| are then adjusted to
// accommodate scroll arrow presentation and window growth.
- (void)adjustMetrics:(LayoutMetrics*)layoutMetrics;

// Support function for adjustMetrics: which calculates the layout changes
// required to accommodate changes in the position and scrollability
// of the top of the folder menu window.
- (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics;

// Support function for adjustMetrics: which calculates the layout changes
// required to accommodate changes in the position and scrollability
// of the bottom of the folder menu window.
- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics;

// Support function for adjustWindowLeft:size:scrollingBy: which applies
// the layout adjustments to the folder menu window and subviews.
- (void)applyMetrics:(LayoutMetrics*)layoutMetrics;

// This function is called when buttons are added or removed from the folder
// menu, and which may require a change in the layout of the folder menu
// window. Such layout changes may include horizontal placement, width,
// height, and scroller visibility changes. (This function calls through
// to -[adjustWindowLeft:size:scrollingBy:].)
// |buttonCount| should contain the updated count of menu buttons.
- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount;

// A helper function which takes the desired amount to scroll, given by
// |scrollDelta|, and calculates the actual scrolling change to be applied
// taking into account the layout of the folder menu window and any
// changes in it's scrollability. (For example, when scrolling down and the
// top-most menu item is coming into view we will only scroll enough for
// that item to be completely presented, which may be less than the
// scroll amount requested.)
- (CGFloat)determineFinalScrollDelta:(CGFloat)scrollDelta;

// |point| is in the base coordinate system of the destination window;
// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
// made and inserted into the new location while leaving the bookmark in
// the old location, otherwise move the bookmark by removing from its old
// location and inserting into the new location.
- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
                  to:(NSPoint)point
                copy:(BOOL)copy;

@end

@interface BookmarkButton (BookmarkBarFolderMenuHighlighting)

// Make the button's border frame always appear when |forceOn| is YES,
// otherwise only border the button when the mouse is inside the button.
- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;

@end

@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)

- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
  [self setShowsBorderOnlyWhileMouseInside:!forceOn];
  [self setNeedsDisplay];
}

@end

@implementation BookmarkBarFolderController

@synthesize subFolderGrowthToRight = subFolderGrowthToRight_;

- (id)initWithParentButton:(BookmarkButton*)button
          parentController:(BookmarkBarFolderController*)parentController
             barController:(BookmarkBarController*)barController {
  NSString* nibPath =
      [base::mac::FrameworkBundle() pathForResource:@"BookmarkBarFolderWindow"
                                             ofType:@"nib"];
  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
    parentButton_.reset([button retain]);
    selectedIndex_ = -1;

    // We want the button to remain bordered as part of the menu path.
    [button forceButtonBorderToStayOnAlways:YES];

    parentController_.reset([parentController retain]);
    if (!parentController_)
      [self setSubFolderGrowthToRight:YES];
    else
      [self setSubFolderGrowthToRight:[parentController
                                        subFolderGrowthToRight]];
    barController_ = barController;  // WEAK
    buttons_.reset([[NSMutableArray alloc] init]);
    folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
    [self configureWindow];
    hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
  }
  return self;
}

- (void)dealloc {
  [self clearInputText];

  // The button is no longer part of the menu path.
  [parentButton_ forceButtonBorderToStayOnAlways:NO];
  [parentButton_ setNeedsDisplay];

  [self removeScrollTracking];
  [self endScroll];
  [hoverState_ draggingExited];

  // Delegate pattern does not retain; make sure pointers to us are removed.
  for (BookmarkButton* button in buttons_.get()) {
    [button setDelegate:nil];
    [button setTarget:nil];
    [button setAction:nil];
  }
  [buttonMenu_ setDelegate:nil];
  [folderMenu_ setDelegate:nil];

  // Note: we don't need to
  //   [NSObject cancelPreviousPerformRequestsWithTarget:self];
  // Because all of our performSelector: calls use withDelay: which
  // retains us.
  [super dealloc];
}

- (void)awakeFromNib {
  NSRect windowFrame = [[self window] frame];
  NSRect scrollViewFrame = [scrollView_ frame];
  padding_ = NSWidth(windowFrame) - NSWidth(scrollViewFrame);
  verticalScrollArrowHeight_ = NSHeight([scrollUpArrowView_ frame]);
}

// Overriden from NSWindowController to call childFolderWillShow: before showing
// the window.
- (void)showWindow:(id)sender {
  [barController_ childFolderWillShow:self];
  [super showWindow:sender];
}

- (int)buttonCount {
  return [[self buttons] count];
}

- (BookmarkButton*)parentButton {
  return parentButton_.get();
}

- (void)offsetFolderMenuWindow:(NSSize)offset {
  NSWindow* window = [self window];
  NSRect windowFrame = [window frame];
  windowFrame.origin.x -= offset.width;
  windowFrame.origin.y += offset.height;  // Yes, in the opposite direction!
  [window setFrame:windowFrame display:YES];
  [folderController_ offsetFolderMenuWindow:offset];
}

- (void)reconfigureMenu {
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
  for (BookmarkButton* button in buttons_.get()) {
    [button setDelegate:nil];
    [button removeFromSuperview];
  }
  [buttons_ removeAllObjects];
  [self configureWindow];
}

#pragma mark Private Methods

- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
  NSImage* image = child ? [barController_ faviconForNode:child] : nil;
  NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil;
  BookmarkBarFolderButtonCell* cell =
      [BookmarkBarFolderButtonCell buttonCellForNode:child
                                         contextMenu:menu
                                            cellText:nil
                                           cellImage:image];
  [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
  return cell;
}

// Redirect to our logic shared with BookmarkBarController.
- (IBAction)openBookmarkFolderFromButton:(id)sender {
  [folderTarget_ openBookmarkFolderFromButton:sender];
}

// Create a bookmark button for the given node using frame.
//
// If |node| is NULL this is an "(empty)" button.
// Does NOT add this button to our button list.
// Returns an autoreleased button.
// Adjusts the input frame width as appropriate.
//
// TODO(jrg): combine with addNodesToButtonList: code from
// bookmark_bar_controller.mm, and generalize that to use both x and y
// offsets.
// http://crbug.com/35966
- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node
                               frame:(NSRect)frame {
  BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
  DCHECK(cell);

  // We must decide if we draw the folder arrow before we ask the cell
  // how big it needs to be.
  if (node && node->is_folder()) {
    // Warning when combining code with bookmark_bar_controller.mm:
    // this call should NOT be made for the bar buttons; only for the
    // subfolder buttons.
    [cell setDrawFolderArrow:YES];
  }

  // The "+2" is needed because, sometimes, Cocoa is off by a tad when
  // returning the value it thinks it needs.
  CGFloat desired = [cell cellSize].width + 2;
  // The width is determined from the maximum of the proposed width
  // (provided in |frame|) or the natural width of the title, then
  // limited by the abolute minimum and maximum allowable widths.
  frame.size.width =
      std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
                        std::max(frame.size.width, desired)),
               bookmarks::kBookmarkMenuButtonMaximumWidth);

  BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
                               autorelease];
  DCHECK(button);

  [button setCell:cell];
  [button setDelegate:self];
  if (node) {
    if (node->is_folder()) {
      [button setTarget:self];
      [button setAction:@selector(openBookmarkFolderFromButton:)];
    } else {
      // Make the button do something.
      [button setTarget:self];
      [button setAction:@selector(openBookmark:)];
      // Add a tooltip.
      [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
      [button setAcceptsTrackIn:YES];
    }
  } else {
    [button setEnabled:NO];
    [button setBordered:NO];
  }
  return button;
}

- (id)folderTarget {
  return folderTarget_.get();
}


// Our parent controller is another BookmarkBarFolderController, so
// our window is to the right or left of it.  We use a little overlap
// since it looks much more menu-like than with none.  If we would
// grow off the screen, switch growth to the other direction.  Growth
// direction sticks for folder windows which are descendents of us.
// If we have tried both directions and neither fits, degrade to a
// default.
- (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
  // We may legitimately need to try two times (growth to right and
  // left but not in that order).  Limit us to three tries in case
  // the folder window can't fit on either side of the screen; we
  // don't want to loop forever.
  CGFloat x;
  int tries = 0;
  while (tries < 2) {
    // Try to grow right.
    if ([self subFolderGrowthToRight]) {
      tries++;
      x = NSMaxX([[parentButton_ window] frame]) -
          bookmarks::kBookmarkMenuOverlap;
      // If off the screen, switch direction.
      if ((x + windowWidth +
           bookmarks::kBookmarkHorizontalScreenPadding) >
          NSMaxX([[[self window] screen] visibleFrame])) {
        [self setSubFolderGrowthToRight:NO];
      } else {
        return x;
      }
    }
    // Try to grow left.
    if (![self subFolderGrowthToRight]) {
      tries++;
      x = NSMinX([[parentButton_ window] frame]) +
          bookmarks::kBookmarkMenuOverlap -
          windowWidth;
      // If off the screen, switch direction.
      if (x < NSMinX([[[self window] screen] visibleFrame])) {
        [self setSubFolderGrowthToRight:YES];
      } else {
        return x;
      }
    }
  }
  // Unhappy; do the best we can.
  return NSMaxX([[[self window] screen] visibleFrame]) - windowWidth;
}


// Compute and return the top left point of our window (screen
// coordinates).  The top left is positioned in a manner similar to
// cascading menus.  Windows may grow to either the right or left of
// their parent (if a sub-folder) so we need to know |windowWidth|.
- (NSPoint)windowTopLeftForWidth:(int)windowWidth height:(int)windowHeight {
  CGFloat kMinSqueezedMenuHeight = bookmarks::kBookmarkFolderButtonHeight * 2.0;
  NSPoint newWindowTopLeft;
  if (![parentController_ isKindOfClass:[self class]]) {
    // If we're not popping up from one of ourselves, we must be
    // popping up from the bookmark bar itself.  In this case, start
    // BELOW the parent button.  Our left is the button left; our top
    // is bottom of button's parent view.
    NSPoint buttonBottomLeftInScreen =
        [[parentButton_ window]
            convertBaseToScreen:[parentButton_
                                    convertPoint:NSZeroPoint toView:nil]];
    NSPoint bookmarkBarBottomLeftInScreen =
        [[parentButton_ window]
            convertBaseToScreen:[[parentButton_ superview]
                                    convertPoint:NSZeroPoint toView:nil]];
    newWindowTopLeft = NSMakePoint(
        buttonBottomLeftInScreen.x + bookmarks::kBookmarkBarButtonOffset,
        bookmarkBarBottomLeftInScreen.y + bookmarks::kBookmarkBarMenuOffset);
    // Make sure the window is on-screen; if not, push left.  It is
    // intentional that top level folders "push left" slightly
    // different than subfolders.
    NSRect screenFrame = [[[parentButton_ window] screen] visibleFrame];
    CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
    if (spillOff > 0.0) {
      newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
                                    NSMinX(screenFrame));
    }
    // The menu looks bad when it is squeezed up against the bottom of the
    // screen and ends up being only a few pixels tall. If it meets the
    // threshold for this case, instead show the menu above the button.
    NSRect visFrame = [[[parentButton_ window] screen] visibleFrame];
    CGFloat availableVerticalSpace = newWindowTopLeft.y -
        (NSMinY(visFrame) + bookmarks::kScrollWindowVerticalMargin);
    if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
        (windowHeight > availableVerticalSpace)) {
      newWindowTopLeft.y = std::min(
          newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
          NSMaxY(visFrame));
    }
  } else {
    // Parent is a folder: expose as much as we can vertically; grow right/left.
    newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
    NSPoint topOfWindow = NSMakePoint(0,
                                      NSMaxY([parentButton_ frame]) -
                                          bookmarks::kBookmarkVerticalPadding);
    topOfWindow = [[parentButton_ window]
                   convertBaseToScreen:[[parentButton_ superview]
                                        convertPoint:topOfWindow toView:nil]];
    newWindowTopLeft.y = topOfWindow.y +
                         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([[window screen] visibleFrame]) +
                     bookmarks::kScrollWindowVerticalMargin;
  metrics.screenBottomY = NSMinY([[window screen] 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);
  NSScreen* screen = [[self window] screen];
  CGFloat effectiveFolderY = metrics.folderY;
  if (!metrics.couldScrollUp && !metrics.couldScrollDown)
    effectiveFolderY -= metrics.windowSize.height;
  metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
  CGFloat maximumY =
      NSMaxY([screen visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
  metrics.canScrollDown = metrics.folderTop > maximumY;

  // Accommodate changes in the bottom of the menu.
  [self adjustMetricsForMenuBottomChanges:layoutMetrics];

  // Accommodate changes in the top of the menu.
  [self adjustMetricsForMenuTopChanges:layoutMetrics];

  metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
  metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
  metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
  metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
  metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
      metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
  metrics.windowFrame.origin.y += metrics.deltaWindowY;
  metrics.windowFrame.origin.x = metrics.windowLeft;
  metrics.windowFrame.size.height += metrics.deltaWindowHeight;
  metrics.windowFrame.size.width = metrics.windowSize.width;
}

- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
  LayoutMetrics& metrics(*layoutMetrics);
  if (metrics.canScrollUp) {
    if (!metrics.couldScrollUp) {
      // Couldn't -> Can
      metrics.deltaWindowY = metrics.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 window] screen] 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:[buttons_ count]];
  CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
  // If the height has changed then also change the origin, and adjust the
  // scroll (if scrolling).
  if ([self canScrollUp]) {
    NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
    scrollPoint.y += deltaMenuHeight;
    [[scrollView_ documentView] scrollPoint:scrollPoint];
  }
  folderFrame.size.height += deltaMenuHeight;
  [folderView_ setFrameSize:folderFrame.size];
  CGFloat windowWidth = [self adjustButtonWidths] + padding_;
  NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
                                                  height:deltaMenuHeight];
  CGFloat left = newWindowTopLeft.x;
  NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
  [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
}

// Determine window size and position.
// Create buttons for all our nodes.
// TODO(jrg): break up into more and smaller routines for easier unit testing.
- (void)configureWindow {
  const BookmarkNode* node = [parentButton_ bookmarkNode];
  DCHECK(node);
  int startingIndex = [[parentButton_ cell] startingChildIndex];
  DCHECK_LE(startingIndex, node->child_count());
  // Must have at least 1 button (for "empty")
  int buttons = std::max(node->child_count() - startingIndex, 1);

  // Prelim height of the window.  We'll trim later as needed.
  int height = [self menuHeightForButtonCount:buttons];
  // We'll need this soon...
  [self window];

  // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
  // http://crbug.com/35966
  NSRect buttonsOuterFrame = NSMakeRect(
      0,
      height - bookmarks::kBookmarkFolderButtonHeight -
          bookmarks::kBookmarkVerticalPadding,
      bookmarks::kDefaultBookmarkWidth,
      bookmarks::kBookmarkFolderButtonHeight);

  // TODO(jrg): combine with addNodesToButtonList: code from
  // bookmark_bar_controller.mm (but use y offset)
  // http://crbug.com/35966
  if (node->empty()) {
    // If no children we are the empty button.
    BookmarkButton* button = [self makeButtonForNode:nil
                                               frame:buttonsOuterFrame];
    [buttons_ addObject:button];
    [folderView_ addSubview:button];
  } else {
    for (int i = startingIndex; i < node->child_count(); ++i) {
      const BookmarkNode* child = node->GetChild(i);
      BookmarkButton* button = [self makeButtonForNode:child
                                                 frame:buttonsOuterFrame];
      [buttons_ addObject:button];
      [folderView_ addSubview:button];
      buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
    }
  }
  [self layOutWindowWithHeight:height];
}

- (void)layOutWindowWithHeight:(CGFloat)height {
  // Lay out the window by adjusting all button widths to be consistent, then
  // base the window width on this ideal button width.
  CGFloat buttonWidth = [self adjustButtonWidths];
  CGFloat windowWidth = buttonWidth + padding_;
  NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
                                                  height:height];

  // Make sure as much of a submenu is exposed (which otherwise would be a
  // problem if the parent button is close to the bottom of the screen).
  if ([parentController_ isKindOfClass:[self class]]) {
    CGFloat minimumY = NSMinY([[[self window] screen] visibleFrame]) +
                       bookmarks::kScrollWindowVerticalMargin +
                       height;
    newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
  }

  NSWindow* window = [self window];
  NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
                                  newWindowTopLeft.y - height,
                                  windowWidth, height);
  [window setFrame:windowFrame display:NO];

  NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
  [folderView_ setFrame:folderFrame];

  // 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:NSMakePoint(0, 0)];

  NSSize newSize = NSMakeSize(windowWidth, 0.0);
  [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0];
  [self configureWindowLevel];

  [window display];
}

// TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
- (CGFloat)adjustButtonWidths {
  CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
  // Use the cell's size as the base for determining the desired width of the
  // button rather than the button's current width. -[cell cellSize] always
  // returns the 'optimum' size of the cell based on the cell's contents even
  // if it's less than the current button size. Relying on the button size
  // would result in buttons that could only get wider but we want to handle
  // the case where the widest button gets removed from a folder menu.
  for (BookmarkButton* button in buttons_.get())
    width = std::max(width, [[button cell] cellSize].width);
  width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
  // Things look and feel more menu-like if all the buttons are the
  // full width of the window, especially if there are submenus.
  for (BookmarkButton* button in buttons_.get()) {
    NSRect buttonFrame = [button frame];
    buttonFrame.size.width = width;
    [button setFrame:buttonFrame];
  }
  return width;
}

// Start a "scroll up" timer.
- (void)beginScrollWindowUp {
  [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
}

// Start a "scroll down" timer.
- (void)beginScrollWindowDown {
  [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
}

// End a scrolling timer.  Can be called excessively with no harm.
- (void)endScroll {
  if (scrollTimer_) {
    [scrollTimer_ invalidate];
    scrollTimer_ = nil;
    verticalScrollDelta_ = 0;
  }
}

- (int)indexOfButton:(BookmarkButton*)button {
  if (button == nil)
    return -1;
  int index = [buttons_ indexOfObject:button];
  return (index == NSNotFound) ? -1 : index;
}

- (BookmarkButton*)buttonAtIndex:(int)which {
  if (which < 0 || which >= [self buttonCount])
    return nil;
  return [buttons_ objectAtIndex:which];
}

// Private, called by performOneScroll only.
// If the button at index contains the mouse it will select it and return YES.
// Otherwise returns NO.
- (BOOL)selectButtonIfHoveredAtIndex:(int)index {
  BookmarkButton* 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.
- (void)performOneScroll:(CGFloat)delta {
  if (delta == 0.0)
    return;
  CGFloat finalDelta = [self determineFinalScrollDelta:delta];
  if (finalDelta == 0.0)
    return;
  int index = [self indexOfButton:buttonThatMouseIsIn_];<commit_msg>Fix chrome/browser_ui build error on mac 64-bit.<commit_after>// 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 "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/event_utils.h"
#include "ui/base/theme_provider.h"

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) {}
};

}  // namespace


// Required to set the right tracking bounds for our fake menus.
@interface NSView(Private)
- (void)_updateTrackingAreas;
@end

@interface BookmarkBarFolderController(Private)
- (void)configureWindow;
- (void)addOrUpdateScrollTracking;
- (void)removeScrollTracking;
- (void)endScroll;
- (void)addScrollTimerWithDelta:(CGFloat)delta;

// Helper function to configureWindow which performs a basic layout of
// the window subviews, in particular the menu buttons and the window width.
- (void)layOutWindowWithHeight:(CGFloat)height;

// Determine the best button width (which will be the widest button or the
// maximum allowable button width, whichever is less) and resize all buttons.
// Return the new width so that the window can be adjusted.
- (CGFloat)adjustButtonWidths;

// Returns the total menu height needed to display |buttonCount| buttons.
// Does not do any fancy tricks like trimming the height to fit on the screen.
- (int)menuHeightForButtonCount:(int)buttonCount;

// Adjust layout of the folder menu window components, showing/hiding the
// scroll up/down arrows, and resizing as necessary for a proper disaplay.
// In order to reduce window flicker, all layout changes are deferred until
// the final step of the adjustment. To accommodate this deferral, window
// height and width changes needed by callers to this function pass their
// desired window changes in |size|. When scrolling is to be performed
// any scrolling change is given by |scrollDelta|. The ultimate amount of
// scrolling may be different from |scrollDelta| in order to accommodate
// changes in the scroller view layout. These proposed window adjustments
// are passed to helper functions using a LayoutMetrics structure.
//
// This function should be called when: 1) initially setting up a folder menu
// window, 2) responding to scrolling of the contents (which may affect the
// height of the window), 3) addition or removal of bookmark items (such as
// during cut/paste/delete/drag/drop operations).
- (void)adjustWindowLeft:(CGFloat)windowLeft
                    size:(NSSize)windowSize
             scrollingBy:(CGFloat)scrollDelta;

// Support function for adjustWindowLeft:size:scrollingBy: which initializes
// the layout adjustments by gathering current folder menu window and subviews
// positions and sizes. This information is set in the |layoutMetrics|
// structure.
- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics;

// Support function for adjustWindowLeft:size:scrollingBy: which calculates
// the changes which must be applied to the folder menu window and subviews
// positions and sizes. |layoutMetrics| contains the proposed window size
// and scrolling along with the other current window and subview layout
// information. The values in |layoutMetrics| are then adjusted to
// accommodate scroll arrow presentation and window growth.
- (void)adjustMetrics:(LayoutMetrics*)layoutMetrics;

// Support function for adjustMetrics: which calculates the layout changes
// required to accommodate changes in the position and scrollability
// of the top of the folder menu window.
- (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics;

// Support function for adjustMetrics: which calculates the layout changes
// required to accommodate changes in the position and scrollability
// of the bottom of the folder menu window.
- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics;

// Support function for adjustWindowLeft:size:scrollingBy: which applies
// the layout adjustments to the folder menu window and subviews.
- (void)applyMetrics:(LayoutMetrics*)layoutMetrics;

// This function is called when buttons are added or removed from the folder
// menu, and which may require a change in the layout of the folder menu
// window. Such layout changes may include horizontal placement, width,
// height, and scroller visibility changes. (This function calls through
// to -[adjustWindowLeft:size:scrollingBy:].)
// |buttonCount| should contain the updated count of menu buttons.
- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount;

// A helper function which takes the desired amount to scroll, given by
// |scrollDelta|, and calculates the actual scrolling change to be applied
// taking into account the layout of the folder menu window and any
// changes in it's scrollability. (For example, when scrolling down and the
// top-most menu item is coming into view we will only scroll enough for
// that item to be completely presented, which may be less than the
// scroll amount requested.)
- (CGFloat)determineFinalScrollDelta:(CGFloat)scrollDelta;

// |point| is in the base coordinate system of the destination window;
// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
// made and inserted into the new location while leaving the bookmark in
// the old location, otherwise move the bookmark by removing from its old
// location and inserting into the new location.
- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
                  to:(NSPoint)point
                copy:(BOOL)copy;

@end

@interface BookmarkButton (BookmarkBarFolderMenuHighlighting)

// Make the button's border frame always appear when |forceOn| is YES,
// otherwise only border the button when the mouse is inside the button.
- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;

@end

@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)

- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
  [self setShowsBorderOnlyWhileMouseInside:!forceOn];
  [self setNeedsDisplay];
}

@end

@implementation BookmarkBarFolderController

@synthesize subFolderGrowthToRight = subFolderGrowthToRight_;

- (id)initWithParentButton:(BookmarkButton*)button
          parentController:(BookmarkBarFolderController*)parentController
             barController:(BookmarkBarController*)barController {
  NSString* nibPath =
      [base::mac::FrameworkBundle() pathForResource:@"BookmarkBarFolderWindow"
                                             ofType:@"nib"];
  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
    parentButton_.reset([button retain]);
    selectedIndex_ = -1;

    // We want the button to remain bordered as part of the menu path.
    [button forceButtonBorderToStayOnAlways:YES];

    parentController_.reset([parentController retain]);
    if (!parentController_)
      [self setSubFolderGrowthToRight:YES];
    else
      [self setSubFolderGrowthToRight:[parentController
                                        subFolderGrowthToRight]];
    barController_ = barController;  // WEAK
    buttons_.reset([[NSMutableArray alloc] init]);
    folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
    [self configureWindow];
    hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
  }
  return self;
}

- (void)dealloc {
  [self clearInputText];

  // The button is no longer part of the menu path.
  [parentButton_ forceButtonBorderToStayOnAlways:NO];
  [parentButton_ setNeedsDisplay];

  [self removeScrollTracking];
  [self endScroll];
  [hoverState_ draggingExited];

  // Delegate pattern does not retain; make sure pointers to us are removed.
  for (BookmarkButton* button in buttons_.get()) {
    [button setDelegate:nil];
    [button setTarget:nil];
    [button setAction:nil];
  }
  [buttonMenu_ setDelegate:nil];
  [folderMenu_ setDelegate:nil];

  // Note: we don't need to
  //   [NSObject cancelPreviousPerformRequestsWithTarget:self];
  // Because all of our performSelector: calls use withDelay: which
  // retains us.
  [super dealloc];
}

- (void)awakeFromNib {
  NSRect windowFrame = [[self window] frame];
  NSRect scrollViewFrame = [scrollView_ frame];
  padding_ = NSWidth(windowFrame) - NSWidth(scrollViewFrame);
  verticalScrollArrowHeight_ = NSHeight([scrollUpArrowView_ frame]);
}

// Overriden from NSWindowController to call childFolderWillShow: before showing
// the window.
- (void)showWindow:(id)sender {
  [barController_ childFolderWillShow:self];
  [super showWindow:sender];
}

- (int)buttonCount {
  return [[self buttons] count];
}

- (BookmarkButton*)parentButton {
  return parentButton_.get();
}

- (void)offsetFolderMenuWindow:(NSSize)offset {
  NSWindow* window = [self window];
  NSRect windowFrame = [window frame];
  windowFrame.origin.x -= offset.width;
  windowFrame.origin.y += offset.height;  // Yes, in the opposite direction!
  [window setFrame:windowFrame display:YES];
  [folderController_ offsetFolderMenuWindow:offset];
}

- (void)reconfigureMenu {
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
  for (BookmarkButton* button in buttons_.get()) {
    [button setDelegate:nil];
    [button removeFromSuperview];
  }
  [buttons_ removeAllObjects];
  [self configureWindow];
}

#pragma mark Private Methods

- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
  NSImage* image = child ? [barController_ faviconForNode:child] : nil;
  NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil;
  BookmarkBarFolderButtonCell* cell =
      [BookmarkBarFolderButtonCell buttonCellForNode:child
                                         contextMenu:menu
                                            cellText:nil
                                           cellImage:image];
  [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
  return cell;
}

// Redirect to our logic shared with BookmarkBarController.
- (IBAction)openBookmarkFolderFromButton:(id)sender {
  [folderTarget_ openBookmarkFolderFromButton:sender];
}

// Create a bookmark button for the given node using frame.
//
// If |node| is NULL this is an "(empty)" button.
// Does NOT add this button to our button list.
// Returns an autoreleased button.
// Adjusts the input frame width as appropriate.
//
// TODO(jrg): combine with addNodesToButtonList: code from
// bookmark_bar_controller.mm, and generalize that to use both x and y
// offsets.
// http://crbug.com/35966
- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node
                               frame:(NSRect)frame {
  BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
  DCHECK(cell);

  // We must decide if we draw the folder arrow before we ask the cell
  // how big it needs to be.
  if (node && node->is_folder()) {
    // Warning when combining code with bookmark_bar_controller.mm:
    // this call should NOT be made for the bar buttons; only for the
    // subfolder buttons.
    [cell setDrawFolderArrow:YES];
  }

  // The "+2" is needed because, sometimes, Cocoa is off by a tad when
  // returning the value it thinks it needs.
  CGFloat desired = [cell cellSize].width + 2;
  // The width is determined from the maximum of the proposed width
  // (provided in |frame|) or the natural width of the title, then
  // limited by the abolute minimum and maximum allowable widths.
  frame.size.width =
      std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
                        std::max(frame.size.width, desired)),
               bookmarks::kBookmarkMenuButtonMaximumWidth);

  BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
                               autorelease];
  DCHECK(button);

  [button setCell:cell];
  [button setDelegate:self];
  if (node) {
    if (node->is_folder()) {
      [button setTarget:self];
      [button setAction:@selector(openBookmarkFolderFromButton:)];
    } else {
      // Make the button do something.
      [button setTarget:self];
      [button setAction:@selector(openBookmark:)];
      // Add a tooltip.
      [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
      [button setAcceptsTrackIn:YES];
    }
  } else {
    [button setEnabled:NO];
    [button setBordered:NO];
  }
  return button;
}

- (id)folderTarget {
  return folderTarget_.get();
}


// Our parent controller is another BookmarkBarFolderController, so
// our window is to the right or left of it.  We use a little overlap
// since it looks much more menu-like than with none.  If we would
// grow off the screen, switch growth to the other direction.  Growth
// direction sticks for folder windows which are descendents of us.
// If we have tried both directions and neither fits, degrade to a
// default.
- (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
  // We may legitimately need to try two times (growth to right and
  // left but not in that order).  Limit us to three tries in case
  // the folder window can't fit on either side of the screen; we
  // don't want to loop forever.
  CGFloat x;
  int tries = 0;
  while (tries < 2) {
    // Try to grow right.
    if ([self subFolderGrowthToRight]) {
      tries++;
      x = NSMaxX([[parentButton_ window] frame]) -
          bookmarks::kBookmarkMenuOverlap;
      // If off the screen, switch direction.
      if ((x + windowWidth +
           bookmarks::kBookmarkHorizontalScreenPadding) >
          NSMaxX([[[self window] screen] visibleFrame])) {
        [self setSubFolderGrowthToRight:NO];
      } else {
        return x;
      }
    }
    // Try to grow left.
    if (![self subFolderGrowthToRight]) {
      tries++;
      x = NSMinX([[parentButton_ window] frame]) +
          bookmarks::kBookmarkMenuOverlap -
          windowWidth;
      // If off the screen, switch direction.
      if (x < NSMinX([[[self window] screen] visibleFrame])) {
        [self setSubFolderGrowthToRight:YES];
      } else {
        return x;
      }
    }
  }
  // Unhappy; do the best we can.
  return NSMaxX([[[self window] screen] visibleFrame]) - windowWidth;
}


// Compute and return the top left point of our window (screen
// coordinates).  The top left is positioned in a manner similar to
// cascading menus.  Windows may grow to either the right or left of
// their parent (if a sub-folder) so we need to know |windowWidth|.
- (NSPoint)windowTopLeftForWidth:(int)windowWidth height:(int)windowHeight {
  CGFloat kMinSqueezedMenuHeight = bookmarks::kBookmarkFolderButtonHeight * 2.0;
  NSPoint newWindowTopLeft;
  if (![parentController_ isKindOfClass:[self class]]) {
    // If we're not popping up from one of ourselves, we must be
    // popping up from the bookmark bar itself.  In this case, start
    // BELOW the parent button.  Our left is the button left; our top
    // is bottom of button's parent view.
    NSPoint buttonBottomLeftInScreen =
        [[parentButton_ window]
            convertBaseToScreen:[parentButton_
                                    convertPoint:NSZeroPoint toView:nil]];
    NSPoint bookmarkBarBottomLeftInScreen =
        [[parentButton_ window]
            convertBaseToScreen:[[parentButton_ superview]
                                    convertPoint:NSZeroPoint toView:nil]];
    newWindowTopLeft = NSMakePoint(
        buttonBottomLeftInScreen.x + bookmarks::kBookmarkBarButtonOffset,
        bookmarkBarBottomLeftInScreen.y + bookmarks::kBookmarkBarMenuOffset);
    // Make sure the window is on-screen; if not, push left.  It is
    // intentional that top level folders "push left" slightly
    // different than subfolders.
    NSRect screenFrame = [[[parentButton_ window] screen] visibleFrame];
    CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
    if (spillOff > 0.0) {
      newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
                                    NSMinX(screenFrame));
    }
    // The menu looks bad when it is squeezed up against the bottom of the
    // screen and ends up being only a few pixels tall. If it meets the
    // threshold for this case, instead show the menu above the button.
    NSRect visFrame = [[[parentButton_ window] screen] visibleFrame];
    CGFloat availableVerticalSpace = newWindowTopLeft.y -
        (NSMinY(visFrame) + bookmarks::kScrollWindowVerticalMargin);
    if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
        (windowHeight > availableVerticalSpace)) {
      newWindowTopLeft.y = std::min(
          newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
          NSMaxY(visFrame));
    }
  } else {
    // Parent is a folder: expose as much as we can vertically; grow right/left.
    newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
    NSPoint topOfWindow = NSMakePoint(0,
                                      NSMaxY([parentButton_ frame]) -
                                          bookmarks::kBookmarkVerticalPadding);
    topOfWindow = [[parentButton_ window]
                   convertBaseToScreen:[[parentButton_ superview]
                                        convertPoint:topOfWindow toView:nil]];
    newWindowTopLeft.y = topOfWindow.y +
                         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([[window screen] visibleFrame]) +
                     bookmarks::kScrollWindowVerticalMargin;
  metrics.screenBottomY = NSMinY([[window screen] 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);
  NSScreen* screen = [[self window] screen];
  CGFloat effectiveFolderY = metrics.folderY;
  if (!metrics.couldScrollUp && !metrics.couldScrollDown)
    effectiveFolderY -= metrics.windowSize.height;
  metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
  CGFloat maximumY =
      NSMaxY([screen visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
  metrics.canScrollDown = metrics.folderTop > maximumY;

  // Accommodate changes in the bottom of the menu.
  [self adjustMetricsForMenuBottomChanges:layoutMetrics];

  // Accommodate changes in the top of the menu.
  [self adjustMetricsForMenuTopChanges:layoutMetrics];

  metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
  metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
  metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
  metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
  metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
      metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
  metrics.windowFrame.origin.y += metrics.deltaWindowY;
  metrics.windowFrame.origin.x = metrics.windowLeft;
  metrics.windowFrame.size.height += metrics.deltaWindowHeight;
  metrics.windowFrame.size.width = metrics.windowSize.width;
}

- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
  LayoutMetrics& metrics(*layoutMetrics);
  if (metrics.canScrollUp) {
    if (!metrics.couldScrollUp) {
      // Couldn't -> Can
      metrics.deltaWindowY = metrics.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 window] screen] 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:[buttons_ count]];
  CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
  // If the height has changed then also change the origin, and adjust the
  // scroll (if scrolling).
  if ([self canScrollUp]) {
    NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
    scrollPoint.y += deltaMenuHeight;
    [[scrollView_ documentView] scrollPoint:scrollPoint];
  }
  folderFrame.size.height += deltaMenuHeight;
  [folderView_ setFrameSize:folderFrame.size];
  CGFloat windowWidth = [self adjustButtonWidths] + padding_;
  NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
                                                  height:deltaMenuHeight];
  CGFloat left = newWindowTopLeft.x;
  NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
  [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
}

// Determine window size and position.
// Create buttons for all our nodes.
// TODO(jrg): break up into more and smaller routines for easier unit testing.
- (void)configureWindow {
  const BookmarkNode* node = [parentButton_ bookmarkNode];
  DCHECK(node);
  int startingIndex = [[parentButton_ cell] startingChildIndex];
  DCHECK_LE(startingIndex, node->child_count());
  // Must have at least 1 button (for "empty")
  int buttons = std::max(node->child_count() - startingIndex, 1);

  // Prelim height of the window.  We'll trim later as needed.
  int height = [self menuHeightForButtonCount:buttons];
  // We'll need this soon...
  [self window];

  // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
  // http://crbug.com/35966
  NSRect buttonsOuterFrame = NSMakeRect(
      0,
      height - bookmarks::kBookmarkFolderButtonHeight -
          bookmarks::kBookmarkVerticalPadding,
      bookmarks::kDefaultBookmarkWidth,
      bookmarks::kBookmarkFolderButtonHeight);

  // TODO(jrg): combine with addNodesToButtonList: code from
  // bookmark_bar_controller.mm (but use y offset)
  // http://crbug.com/35966
  if (node->empty()) {
    // If no children we are the empty button.
    BookmarkButton* button = [self makeButtonForNode:nil
                                               frame:buttonsOuterFrame];
    [buttons_ addObject:button];
    [folderView_ addSubview:button];
  } else {
    for (int i = startingIndex; i < node->child_count(); ++i) {
      const BookmarkNode* child = node->GetChild(i);
      BookmarkButton* button = [self makeButtonForNode:child
                                                 frame:buttonsOuterFrame];
      [buttons_ addObject:button];
      [folderView_ addSubview:button];
      buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
    }
  }
  [self layOutWindowWithHeight:height];
}

- (void)layOutWindowWithHeight:(CGFloat)height {
  // Lay out the window by adjusting all button widths to be consistent, then
  // base the window width on this ideal button width.
  CGFloat buttonWidth = [self adjustButtonWidths];
  CGFloat windowWidth = buttonWidth + padding_;
  NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
                                                  height:height];

  // Make sure as much of a submenu is exposed (which otherwise would be a
  // problem if the parent button is close to the bottom of the screen).
  if ([parentController_ isKindOfClass:[self class]]) {
    CGFloat minimumY = NSMinY([[[self window] screen] visibleFrame]) +
                       bookmarks::kScrollWindowVerticalMargin +
                       height;
    newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
  }

  NSWindow* window = [self window];
  NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
                                  newWindowTopLeft.y - height,
                                  windowWidth, height);
  [window setFrame:windowFrame display:NO];

  NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
  [folderView_ setFrame:folderFrame];

  // 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:NSMakePoint(0, 0)];

  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.
- (void)performOneScroll:(CGFloat)delta {
  if (delta == 0.0)
    return;
  CGFloat finalDelta = [self determineFinalScrollDelta:delta];
  if (finalDelta == 0.0)
    return;
  int index = [self indexOfButton:buttonThatMouseIsIn_];
{'fraction_non_alphanumeric': 0.09171106764686046, 'fraction_numerical': 0.0031564038578269374, 'mean_word_length': 3.632920884889389, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 52, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '16397', 'n_tokens_mistral': 21139, 'n_tokens_neox': 20451, 'n_words': 6431}