• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
6
7#include "base/mac/mac_util.h"
8#include "base/sys_string_conversions.h"
9#include "chrome/browser/bookmarks/bookmark_model.h"
10#include "chrome/browser/bookmarks/bookmark_utils.h"
11#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
12#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
13#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
14#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
15#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
16#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
17#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
18#import "chrome/browser/ui/cocoa/browser_window_controller.h"
19#import "chrome/browser/ui/cocoa/event_utils.h"
20#include "ui/base/theme_provider.h"
21
22using bookmarks::kBookmarkBarMenuCornerRadius;
23
24namespace {
25
26// Frequency of the scrolling timer in seconds.
27const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1;
28
29// Amount to scroll by per timer fire.  We scroll rather slowly; to
30// accomodate we do several at a time.
31const CGFloat kBookmarkBarFolderScrollAmount =
32    3 * bookmarks::kBookmarkFolderButtonHeight;
33
34// Amount to scroll for each scroll wheel roll.
35const CGFloat kBookmarkBarFolderScrollWheelAmount =
36    1 * bookmarks::kBookmarkFolderButtonHeight;
37
38// Determining adjustments to the layout of the folder menu window in response
39// to resizing and scrolling relies on many visual factors. The following
40// struct is used to pass around these factors to the several support
41// functions involved in the adjustment calculations and application.
42struct LayoutMetrics {
43  // Metrics applied during the final layout adjustments to the window,
44  // the main visible content view, and the menu content view (i.e. the
45  // scroll view).
46  CGFloat windowLeft;
47  NSSize windowSize;
48  // The proposed and then final scrolling adjustment made to the scrollable
49  // area of the folder menu. This may be modified during the window layout
50  // primarily as a result of hiding or showing the scroll arrows.
51  CGFloat scrollDelta;
52  NSRect windowFrame;
53  NSRect visibleFrame;
54  NSRect scrollerFrame;
55  NSPoint scrollPoint;
56  // The difference between 'could' and 'can' in these next four data members
57  // is this: 'could' represents the previous condition for scrollability
58  // while 'can' represents what the new condition will be for scrollability.
59  BOOL couldScrollUp;
60  BOOL canScrollUp;
61  BOOL couldScrollDown;
62  BOOL canScrollDown;
63  // Determines the optimal time during folder menu layout when the contents
64  // of the button scroll area should be scrolled in order to prevent
65  // flickering.
66  BOOL preScroll;
67
68  // Intermediate metrics used in determining window vertical layout changes.
69  CGFloat deltaWindowHeight;
70  CGFloat deltaWindowY;
71  CGFloat deltaVisibleHeight;
72  CGFloat deltaVisibleY;
73  CGFloat deltaScrollerHeight;
74  CGFloat deltaScrollerY;
75
76  // Convenience metrics used in multiple functions (carried along here in
77  // order to eliminate the need to calculate in multiple places and
78  // reduce the possibility of bugs).
79  CGFloat minimumY;
80  CGFloat oldWindowY;
81  CGFloat folderY;
82  CGFloat folderTop;
83
84  LayoutMetrics(CGFloat windowLeft, NSSize windowSize, CGFloat scrollDelta) :
85    windowLeft(windowLeft),
86    windowSize(windowSize),
87    scrollDelta(scrollDelta),
88    couldScrollUp(NO),
89    canScrollUp(NO),
90    couldScrollDown(NO),
91    canScrollDown(NO),
92    preScroll(NO),
93    deltaWindowHeight(0.0),
94    deltaWindowY(0.0),
95    deltaVisibleHeight(0.0),
96    deltaVisibleY(0.0),
97    deltaScrollerHeight(0.0),
98    deltaScrollerY(0.0),
99    oldWindowY(0.0),
100    folderY(0.0),
101    folderTop(0.0) {}
102};
103
104}  // namespace
105
106
107// Required to set the right tracking bounds for our fake menus.
108@interface NSView(Private)
109- (void)_updateTrackingAreas;
110@end
111
112@interface BookmarkBarFolderController(Private)
113- (void)configureWindow;
114- (void)addOrUpdateScrollTracking;
115- (void)removeScrollTracking;
116- (void)endScroll;
117- (void)addScrollTimerWithDelta:(CGFloat)delta;
118
119// Helper function to configureWindow which performs a basic layout of
120// the window subviews, in particular the menu buttons and the window width.
121- (void)layOutWindowWithHeight:(CGFloat)height;
122
123// Determine the best button width (which will be the widest button or the
124// maximum allowable button width, whichever is less) and resize all buttons.
125// Return the new width so that the window can be adjusted.
126- (CGFloat)adjustButtonWidths;
127
128// Returns the total menu height needed to display |buttonCount| buttons.
129// Does not do any fancy tricks like trimming the height to fit on the screen.
130- (int)menuHeightForButtonCount:(int)buttonCount;
131
132// Adjust layout of the folder menu window components, showing/hiding the
133// scroll up/down arrows, and resizing as necessary for a proper disaplay.
134// In order to reduce window flicker, all layout changes are deferred until
135// the final step of the adjustment. To accommodate this deferral, window
136// height and width changes needed by callers to this function pass their
137// desired window changes in |size|. When scrolling is to be performed
138// any scrolling change is given by |scrollDelta|. The ultimate amount of
139// scrolling may be different from |scrollDelta| in order to accommodate
140// changes in the scroller view layout. These proposed window adjustments
141// are passed to helper functions using a LayoutMetrics structure.
142//
143// This function should be called when: 1) initially setting up a folder menu
144// window, 2) responding to scrolling of the contents (which may affect the
145// height of the window), 3) addition or removal of bookmark items (such as
146// during cut/paste/delete/drag/drop operations).
147- (void)adjustWindowLeft:(CGFloat)windowLeft
148                    size:(NSSize)windowSize
149             scrollingBy:(CGFloat)scrollDelta;
150
151// Support function for adjustWindowLeft:size:scrollingBy: which initializes
152// the layout adjustments by gathering current folder menu window and subviews
153// positions and sizes. This information is set in the |layoutMetrics|
154// structure.
155- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics;
156
157// Support function for adjustWindowLeft:size:scrollingBy: which calculates
158// the changes which must be applied to the folder menu window and subviews
159// positions and sizes. |layoutMetrics| contains the proposed window size
160// and scrolling along with the other current window and subview layout
161// information. The values in |layoutMetrics| are then adjusted to
162// accommodate scroll arrow presentation and window growth.
163- (void)adjustMetrics:(LayoutMetrics*)layoutMetrics;
164
165// Support function for adjustMetrics: which calculates the layout changes
166// required to accommodate changes in the position and scrollability
167// of the top of the folder menu window.
168- (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics;
169
170// Support function for adjustMetrics: which calculates the layout changes
171// required to accommodate changes in the position and scrollability
172// of the bottom of the folder menu window.
173- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics;
174
175// Support function for adjustWindowLeft:size:scrollingBy: which applies
176// the layout adjustments to the folder menu window and subviews.
177- (void)applyMetrics:(LayoutMetrics*)layoutMetrics;
178
179// This function is called when buttons are added or removed from the folder
180// menu, and which may require a change in the layout of the folder menu
181// window. Such layout changes may include horizontal placement, width,
182// height, and scroller visibility changes. (This function calls through
183// to -[adjustWindowLeft:size:scrollingBy:].)
184// |buttonCount| should contain the updated count of menu buttons.
185- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount;
186
187// A helper function which takes the desired amount to scroll, given by
188// |scrollDelta|, and calculates the actual scrolling change to be applied
189// taking into account the layout of the folder menu window and any
190// changes in it's scrollability. (For example, when scrolling down and the
191// top-most menu item is coming into view we will only scroll enough for
192// that item to be completely presented, which may be less than the
193// scroll amount requested.)
194- (CGFloat)determineFinalScrollDelta:(CGFloat)scrollDelta;
195
196// |point| is in the base coordinate system of the destination window;
197// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
198// made and inserted into the new location while leaving the bookmark in
199// the old location, otherwise move the bookmark by removing from its old
200// location and inserting into the new location.
201- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
202                  to:(NSPoint)point
203                copy:(BOOL)copy;
204
205@end
206
207@interface BookmarkButton (BookmarkBarFolderMenuHighlighting)
208
209// Make the button's border frame always appear when |forceOn| is YES,
210// otherwise only border the button when the mouse is inside the button.
211- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;
212
213@end
214
215@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)
216
217- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
218  [self setShowsBorderOnlyWhileMouseInside:!forceOn];
219  [self setNeedsDisplay];
220}
221
222@end
223
224@implementation BookmarkBarFolderController
225
226@synthesize subFolderGrowthToRight = subFolderGrowthToRight_;
227
228- (id)initWithParentButton:(BookmarkButton*)button
229          parentController:(BookmarkBarFolderController*)parentController
230             barController:(BookmarkBarController*)barController {
231  NSString* nibPath =
232      [base::mac::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow"
233                                          ofType:@"nib"];
234  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
235    parentButton_.reset([button retain]);
236    selectedIndex_ = -1;
237
238    // We want the button to remain bordered as part of the menu path.
239    [button forceButtonBorderToStayOnAlways:YES];
240
241    parentController_.reset([parentController retain]);
242    if (!parentController_)
243      [self setSubFolderGrowthToRight:YES];
244    else
245      [self setSubFolderGrowthToRight:[parentController
246                                        subFolderGrowthToRight]];
247    barController_ = barController;  // WEAK
248    buttons_.reset([[NSMutableArray alloc] init]);
249    folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
250    [self configureWindow];
251    hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
252  }
253  return self;
254}
255
256- (void)dealloc {
257  [self clearInputText];
258
259  // The button is no longer part of the menu path.
260  [parentButton_ forceButtonBorderToStayOnAlways:NO];
261  [parentButton_ setNeedsDisplay];
262
263  [self removeScrollTracking];
264  [self endScroll];
265  [hoverState_ draggingExited];
266
267  // Delegate pattern does not retain; make sure pointers to us are removed.
268  for (BookmarkButton* button in buttons_.get()) {
269    [button setDelegate:nil];
270    [button setTarget:nil];
271    [button setAction:nil];
272  }
273
274  // Note: we don't need to
275  //   [NSObject cancelPreviousPerformRequestsWithTarget:self];
276  // Because all of our performSelector: calls use withDelay: which
277  // retains us.
278  [super dealloc];
279}
280
281- (void)awakeFromNib {
282  NSRect windowFrame = [[self window] frame];
283  NSRect scrollViewFrame = [scrollView_ frame];
284  padding_ = NSWidth(windowFrame) - NSWidth(scrollViewFrame);
285  verticalScrollArrowHeight_ = NSHeight([scrollUpArrowView_ frame]);
286}
287
288// Overriden from NSWindowController to call childFolderWillShow: before showing
289// the window.
290- (void)showWindow:(id)sender {
291  [barController_ childFolderWillShow:self];
292  [super showWindow:sender];
293}
294
295- (int)buttonCount {
296  return [[self buttons] count];
297}
298
299- (BookmarkButton*)parentButton {
300  return parentButton_.get();
301}
302
303- (void)offsetFolderMenuWindow:(NSSize)offset {
304  NSWindow* window = [self window];
305  NSRect windowFrame = [window frame];
306  windowFrame.origin.x -= offset.width;
307  windowFrame.origin.y += offset.height;  // Yes, in the opposite direction!
308  [window setFrame:windowFrame display:YES];
309  [folderController_ offsetFolderMenuWindow:offset];
310}
311
312- (void)reconfigureMenu {
313  [NSObject cancelPreviousPerformRequestsWithTarget:self];
314  for (BookmarkButton* button in buttons_.get()) {
315    [button setDelegate:nil];
316    [button removeFromSuperview];
317  }
318  [buttons_ removeAllObjects];
319  [self configureWindow];
320}
321
322#pragma mark Private Methods
323
324- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
325  NSImage* image = child ? [barController_ faviconForNode:child] : nil;
326  NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil;
327  BookmarkBarFolderButtonCell* cell =
328      [BookmarkBarFolderButtonCell buttonCellForNode:child
329                                         contextMenu:menu
330                                            cellText:nil
331                                           cellImage:image];
332  [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
333  return cell;
334}
335
336// Redirect to our logic shared with BookmarkBarController.
337- (IBAction)openBookmarkFolderFromButton:(id)sender {
338  [folderTarget_ openBookmarkFolderFromButton:sender];
339}
340
341// Create a bookmark button for the given node using frame.
342//
343// If |node| is NULL this is an "(empty)" button.
344// Does NOT add this button to our button list.
345// Returns an autoreleased button.
346// Adjusts the input frame width as appropriate.
347//
348// TODO(jrg): combine with addNodesToButtonList: code from
349// bookmark_bar_controller.mm, and generalize that to use both x and y
350// offsets.
351// http://crbug.com/35966
352- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node
353                               frame:(NSRect)frame {
354  BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
355  DCHECK(cell);
356
357  // We must decide if we draw the folder arrow before we ask the cell
358  // how big it needs to be.
359  if (node && node->is_folder()) {
360    // Warning when combining code with bookmark_bar_controller.mm:
361    // this call should NOT be made for the bar buttons; only for the
362    // subfolder buttons.
363    [cell setDrawFolderArrow:YES];
364  }
365
366  // The "+2" is needed because, sometimes, Cocoa is off by a tad when
367  // returning the value it thinks it needs.
368  CGFloat desired = [cell cellSize].width + 2;
369  // The width is determined from the maximum of the proposed width
370  // (provided in |frame|) or the natural width of the title, then
371  // limited by the abolute minimum and maximum allowable widths.
372  frame.size.width =
373      std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
374                        std::max(frame.size.width, desired)),
375               bookmarks::kBookmarkMenuButtonMaximumWidth);
376
377  BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
378                               autorelease];
379  DCHECK(button);
380
381  [button setCell:cell];
382  [button setDelegate:self];
383  if (node) {
384    if (node->is_folder()) {
385      [button setTarget:self];
386      [button setAction:@selector(openBookmarkFolderFromButton:)];
387    } else {
388      // Make the button do something.
389      [button setTarget:self];
390      [button setAction:@selector(openBookmark:)];
391      // Add a tooltip.
392      NSString* title = base::SysUTF16ToNSString(node->GetTitle());
393      std::string urlString = node->GetURL().possibly_invalid_spec();
394      NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title,
395                                    urlString.c_str()];
396      [button setToolTip:tooltip];
397      [button setAcceptsTrackIn:YES];
398    }
399  } else {
400    [button setEnabled:NO];
401    [button setBordered:NO];
402  }
403  return button;
404}
405
406- (id)folderTarget {
407  return folderTarget_.get();
408}
409
410
411// Our parent controller is another BookmarkBarFolderController, so
412// our window is to the right or left of it.  We use a little overlap
413// since it looks much more menu-like than with none.  If we would
414// grow off the screen, switch growth to the other direction.  Growth
415// direction sticks for folder windows which are descendents of us.
416// If we have tried both directions and neither fits, degrade to a
417// default.
418- (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
419  // We may legitimately need to try two times (growth to right and
420  // left but not in that order).  Limit us to three tries in case
421  // the folder window can't fit on either side of the screen; we
422  // don't want to loop forever.
423  CGFloat x;
424  int tries = 0;
425  while (tries < 2) {
426    // Try to grow right.
427    if ([self subFolderGrowthToRight]) {
428      tries++;
429      x = NSMaxX([[parentButton_ window] frame]) -
430          bookmarks::kBookmarkMenuOverlap;
431      // If off the screen, switch direction.
432      if ((x + windowWidth +
433           bookmarks::kBookmarkHorizontalScreenPadding) >
434          NSMaxX([[[self window] screen] visibleFrame])) {
435        [self setSubFolderGrowthToRight:NO];
436      } else {
437        return x;
438      }
439    }
440    // Try to grow left.
441    if (![self subFolderGrowthToRight]) {
442      tries++;
443      x = NSMinX([[parentButton_ window] frame]) +
444          bookmarks::kBookmarkMenuOverlap -
445          windowWidth;
446      // If off the screen, switch direction.
447      if (x < NSMinX([[[self window] screen] visibleFrame])) {
448        [self setSubFolderGrowthToRight:YES];
449      } else {
450        return x;
451      }
452    }
453  }
454  // Unhappy; do the best we can.
455  return NSMaxX([[[self window] screen] visibleFrame]) - windowWidth;
456}
457
458
459// Compute and return the top left point of our window (screen
460// coordinates).  The top left is positioned in a manner similar to
461// cascading menus.  Windows may grow to either the right or left of
462// their parent (if a sub-folder) so we need to know |windowWidth|.
463- (NSPoint)windowTopLeftForWidth:(int)windowWidth height:(int)windowHeight {
464  CGFloat kMinSqueezedMenuHeight = bookmarks::kBookmarkFolderButtonHeight * 2.0;
465  NSPoint newWindowTopLeft;
466  if (![parentController_ isKindOfClass:[self class]]) {
467    // If we're not popping up from one of ourselves, we must be
468    // popping up from the bookmark bar itself.  In this case, start
469    // BELOW the parent button.  Our left is the button left; our top
470    // is bottom of button's parent view.
471    NSPoint buttonBottomLeftInScreen =
472        [[parentButton_ window]
473            convertBaseToScreen:[parentButton_
474                                    convertPoint:NSZeroPoint toView:nil]];
475    NSPoint bookmarkBarBottomLeftInScreen =
476        [[parentButton_ window]
477            convertBaseToScreen:[[parentButton_ superview]
478                                    convertPoint:NSZeroPoint toView:nil]];
479    newWindowTopLeft = NSMakePoint(
480        buttonBottomLeftInScreen.x + bookmarks::kBookmarkBarButtonOffset,
481        bookmarkBarBottomLeftInScreen.y + bookmarks::kBookmarkBarMenuOffset);
482    // Make sure the window is on-screen; if not, push left.  It is
483    // intentional that top level folders "push left" slightly
484    // different than subfolders.
485    NSRect screenFrame = [[[parentButton_ window] screen] visibleFrame];
486    CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
487    if (spillOff > 0.0) {
488      newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
489                                    NSMinX(screenFrame));
490    }
491    // The menu looks bad when it is squeezed up against the bottom of the
492    // screen and ends up being only a few pixels tall. If it meets the
493    // threshold for this case, instead show the menu above the button.
494    NSRect visFrame = [[[parentButton_ window] screen] visibleFrame];
495    CGFloat availableVerticalSpace = newWindowTopLeft.y -
496        (NSMinY(visFrame) + bookmarks::kScrollWindowVerticalMargin);
497    if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
498        (windowHeight > availableVerticalSpace)) {
499      newWindowTopLeft.y = std::min(
500          newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
501          NSMaxY(visFrame));
502    }
503  } else {
504    // Parent is a folder: expose as much as we can vertically; grow right/left.
505    newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
506    NSPoint topOfWindow = NSMakePoint(0,
507                                      NSMaxY([parentButton_ frame]) -
508                                          bookmarks::kBookmarkVerticalPadding);
509    topOfWindow = [[parentButton_ window]
510                   convertBaseToScreen:[[parentButton_ superview]
511                                        convertPoint:topOfWindow toView:nil]];
512    newWindowTopLeft.y = topOfWindow.y;
513  }
514  return newWindowTopLeft;
515}
516
517// Set our window level to the right spot so we're above the menubar, dock, etc.
518// Factored out so we can override/noop in a unit test.
519- (void)configureWindowLevel {
520  [[self window] setLevel:NSPopUpMenuWindowLevel];
521}
522
523- (int)menuHeightForButtonCount:(int)buttonCount {
524  // This does not take into account any padding which may be required at the
525  // top and/or bottom of the window.
526  return (buttonCount * bookmarks::kBookmarkFolderButtonHeight) +
527      2 * bookmarks::kBookmarkVerticalPadding;
528}
529
530- (void)adjustWindowLeft:(CGFloat)windowLeft
531                    size:(NSSize)windowSize
532             scrollingBy:(CGFloat)scrollDelta {
533  // Callers of this function should make adjustments to the vertical
534  // attributes of the folder view only (height, scroll position).
535  // This function will then make appropriate layout adjustments in order
536  // to accommodate screen/dock margins, scroll-up and scroll-down arrow
537  // presentation, etc.
538  // The 4 views whose vertical height and origins may be adjusted
539  // by this function are:
540  //  1) window, 2) visible content view, 3) scroller view, 4) folder view.
541
542  LayoutMetrics layoutMetrics(windowLeft, windowSize, scrollDelta);
543  [self gatherMetrics:&layoutMetrics];
544  [self adjustMetrics:&layoutMetrics];
545  [self applyMetrics:&layoutMetrics];
546}
547
548- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics {
549  LayoutMetrics& metrics(*layoutMetrics);
550  NSWindow* window = [self window];
551  metrics.windowFrame = [window frame];
552  metrics.visibleFrame = [visibleView_ frame];
553  metrics.scrollerFrame = [scrollView_ frame];
554  metrics.scrollPoint = [scrollView_ documentVisibleRect].origin;
555  metrics.scrollPoint.y -= metrics.scrollDelta;
556  metrics.couldScrollUp = ![scrollUpArrowView_ isHidden];
557  metrics.couldScrollDown = ![scrollDownArrowView_ isHidden];
558
559  metrics.deltaWindowHeight = 0.0;
560  metrics.deltaWindowY = 0.0;
561  metrics.deltaVisibleHeight = 0.0;
562  metrics.deltaVisibleY = 0.0;
563  metrics.deltaScrollerHeight = 0.0;
564  metrics.deltaScrollerY = 0.0;
565
566  metrics.minimumY = NSMinY([[window screen] visibleFrame]) +
567                     bookmarks::kScrollWindowVerticalMargin;
568  metrics.oldWindowY = NSMinY(metrics.windowFrame);
569  metrics.folderY =
570      metrics.scrollerFrame.origin.y + metrics.visibleFrame.origin.y +
571      metrics.oldWindowY - metrics.scrollPoint.y;
572  metrics.folderTop = metrics.folderY + NSHeight([folderView_ frame]);
573}
574
575- (void)adjustMetrics:(LayoutMetrics*)layoutMetrics {
576  LayoutMetrics& metrics(*layoutMetrics);
577  NSScreen* screen = [[self window] screen];
578  CGFloat effectiveFolderY = metrics.folderY;
579  if (!metrics.couldScrollUp && !metrics.couldScrollDown)
580    effectiveFolderY -= metrics.windowSize.height;
581  metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
582  CGFloat maximumY =
583      NSMaxY([screen visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
584  metrics.canScrollDown = metrics.folderTop > maximumY;
585
586  // Accommodate changes in the bottom of the menu.
587  [self adjustMetricsForMenuBottomChanges:layoutMetrics];
588
589  // Accommodate changes in the top of the menu.
590  [self adjustMetricsForMenuTopChanges:layoutMetrics];
591
592  metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
593  metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
594  metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
595  metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
596  metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
597      metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
598  metrics.windowFrame.origin.y += metrics.deltaWindowY;
599  metrics.windowFrame.origin.x = metrics.windowLeft;
600  metrics.windowFrame.size.height += metrics.deltaWindowHeight;
601  metrics.windowFrame.size.width = metrics.windowSize.width;
602}
603
604- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
605  LayoutMetrics& metrics(*layoutMetrics);
606  if (metrics.canScrollUp) {
607    if (!metrics.couldScrollUp) {
608      // Couldn't -> Can
609      metrics.deltaWindowY = -metrics.oldWindowY;
610      metrics.deltaWindowHeight = -metrics.deltaWindowY;
611      metrics.deltaVisibleY = metrics.minimumY;
612      metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
613      metrics.deltaScrollerY = verticalScrollArrowHeight_;
614      metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
615      // Adjust the scroll delta if we've grown the window and it is
616      // now scroll-up-able, but don't adjust it if we've
617      // scrolled down and it wasn't scroll-up-able but now is.
618      if (metrics.canScrollDown == metrics.couldScrollDown) {
619        CGFloat deltaScroll = metrics.deltaWindowY + metrics.deltaScrollerY +
620                              metrics.deltaVisibleY;
621        metrics.scrollPoint.y += deltaScroll + metrics.windowSize.height;
622      }
623    } else if (!metrics.canScrollDown && metrics.windowSize.height > 0.0) {
624      metrics.scrollPoint.y += metrics.windowSize.height;
625    }
626  } else {
627    if (metrics.couldScrollUp) {
628      // Could -> Can't
629      metrics.deltaWindowY = metrics.folderY - metrics.oldWindowY;
630      metrics.deltaWindowHeight = -metrics.deltaWindowY;
631      metrics.deltaVisibleY = -metrics.visibleFrame.origin.y;
632      metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
633      metrics.deltaScrollerY = -verticalScrollArrowHeight_;
634      metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
635      // We are no longer scroll-up-able so the scroll point drops to zero.
636      metrics.scrollPoint.y = 0.0;
637    } else {
638      // Couldn't -> Can't
639      // Check for menu height change by looking at the relative tops of the
640      // menu folder and the window folder, which previously would have been
641      // the same.
642      metrics.deltaWindowY = NSMaxY(metrics.windowFrame) - metrics.folderTop;
643      metrics.deltaWindowHeight = -metrics.deltaWindowY;
644    }
645  }
646}
647
648- (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics {
649  LayoutMetrics& metrics(*layoutMetrics);
650  if (metrics.canScrollDown == metrics.couldScrollDown) {
651    if (!metrics.canScrollDown) {
652      // Not scroll-down-able but the menu top has changed.
653      metrics.deltaWindowHeight += metrics.scrollDelta;
654    }
655  } else {
656    if (metrics.canScrollDown) {
657      // Couldn't -> Can
658      metrics.deltaWindowHeight += (NSMaxY([[[self window] screen]
659                                    visibleFrame]) -
660                                    NSMaxY(metrics.windowFrame));
661      metrics.deltaVisibleHeight -= bookmarks::kScrollWindowVerticalMargin;
662      metrics.deltaScrollerHeight -= verticalScrollArrowHeight_;
663    } else {
664      // Could -> Can't
665      metrics.deltaWindowHeight -= bookmarks::kScrollWindowVerticalMargin;
666      metrics.deltaVisibleHeight += bookmarks::kScrollWindowVerticalMargin;
667      metrics.deltaScrollerHeight += verticalScrollArrowHeight_;
668    }
669  }
670}
671
672- (void)applyMetrics:(LayoutMetrics*)layoutMetrics {
673  LayoutMetrics& metrics(*layoutMetrics);
674  // Hide or show the scroll arrows.
675  if (metrics.canScrollUp != metrics.couldScrollUp)
676    [scrollUpArrowView_ setHidden:metrics.couldScrollUp];
677  if (metrics.canScrollDown != metrics.couldScrollDown)
678    [scrollDownArrowView_ setHidden:metrics.couldScrollDown];
679
680  // Adjust the geometry. The order is important because of sizer dependencies.
681  [scrollView_ setFrame:metrics.scrollerFrame];
682  [visibleView_ setFrame:metrics.visibleFrame];
683  // This little bit of trickery handles the one special case where
684  // the window is now scroll-up-able _and_ going to be resized -- scroll
685  // first in order to prevent flashing.
686  if (metrics.preScroll)
687    [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
688
689  [[self window] setFrame:metrics.windowFrame display:YES];
690
691  // In all other cases we defer scrolling until the window has been resized
692  // in order to prevent flashing.
693  if (!metrics.preScroll)
694    [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
695
696  // TODO(maf) find a non-SPI way to do this.
697  // Hack. This is the only way I've found to get the tracking area cache
698  // to update properly during a mouse tracking loop.
699  // Without this, the item tracking-areas are wrong when using a scrollable
700  // menu with the mouse held down.
701  NSView *contentView = [[self window] contentView] ;
702  if ([contentView respondsToSelector:@selector(_updateTrackingAreas)])
703    [contentView _updateTrackingAreas];
704
705
706  if (metrics.canScrollUp != metrics.couldScrollUp ||
707      metrics.canScrollDown != metrics.couldScrollDown ||
708      metrics.scrollDelta != 0.0) {
709    if (metrics.canScrollUp || metrics.canScrollDown)
710      [self addOrUpdateScrollTracking];
711    else
712      [self removeScrollTracking];
713  }
714}
715
716- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount {
717  NSRect folderFrame = [folderView_ frame];
718  CGFloat newMenuHeight =
719      (CGFloat)[self menuHeightForButtonCount:[buttons_ count]];
720  CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
721  // If the height has changed then also change the origin, and adjust the
722  // scroll (if scrolling).
723  if ([self canScrollUp]) {
724    NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
725    scrollPoint.y += deltaMenuHeight;
726    [[scrollView_ documentView] scrollPoint:scrollPoint];
727  }
728  folderFrame.size.height += deltaMenuHeight;
729  [folderView_ setFrameSize:folderFrame.size];
730  CGFloat windowWidth = [self adjustButtonWidths] + padding_;
731  NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
732                                                  height:deltaMenuHeight];
733  CGFloat left = newWindowTopLeft.x;
734  NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
735  [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
736}
737
738// Determine window size and position.
739// Create buttons for all our nodes.
740// TODO(jrg): break up into more and smaller routines for easier unit testing.
741- (void)configureWindow {
742  const BookmarkNode* node = [parentButton_ bookmarkNode];
743  DCHECK(node);
744  int startingIndex = [[parentButton_ cell] startingChildIndex];
745  DCHECK_LE(startingIndex, node->child_count());
746  // Must have at least 1 button (for "empty")
747  int buttons = std::max(node->child_count() - startingIndex, 1);
748
749  // Prelim height of the window.  We'll trim later as needed.
750  int height = [self menuHeightForButtonCount:buttons];
751  // We'll need this soon...
752  [self window];
753
754  // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
755  // http://crbug.com/35966
756  NSRect buttonsOuterFrame = NSMakeRect(
757      0,
758      height - bookmarks::kBookmarkFolderButtonHeight -
759          bookmarks::kBookmarkVerticalPadding,
760      bookmarks::kDefaultBookmarkWidth,
761      bookmarks::kBookmarkFolderButtonHeight);
762
763  // TODO(jrg): combine with addNodesToButtonList: code from
764  // bookmark_bar_controller.mm (but use y offset)
765  // http://crbug.com/35966
766  if (!node->child_count()) {
767    // If no children we are the empty button.
768    BookmarkButton* button = [self makeButtonForNode:nil
769                                               frame:buttonsOuterFrame];
770    [buttons_ addObject:button];
771    [folderView_ addSubview:button];
772  } else {
773    for (int i = startingIndex;
774         i < node->child_count();
775         i++) {
776      const BookmarkNode* child = node->GetChild(i);
777      BookmarkButton* button = [self makeButtonForNode:child
778                                                 frame:buttonsOuterFrame];
779      [buttons_ addObject:button];
780      [folderView_ addSubview:button];
781      buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
782    }
783  }
784  [self layOutWindowWithHeight:height];
785}
786
787- (void)layOutWindowWithHeight:(CGFloat)height {
788  // Lay out the window by adjusting all button widths to be consistent, then
789  // base the window width on this ideal button width.
790  CGFloat buttonWidth = [self adjustButtonWidths];
791  CGFloat windowWidth = buttonWidth + padding_;
792  NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
793                                                  height:height];
794  // Make sure as much of a submenu is exposed (which otherwise would be a
795  // problem if the parent button is close to the bottom of the screen).
796  if ([parentController_ isKindOfClass:[self class]]) {
797    CGFloat minimumY = NSMinY([[[self window] screen] visibleFrame]) +
798                       bookmarks::kScrollWindowVerticalMargin +
799                       height;
800    newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
801  }
802  NSWindow* window = [self window];
803  NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
804                                  newWindowTopLeft.y - height,
805                                  windowWidth, height);
806  [window setFrame:windowFrame display:NO];
807  NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
808  [folderView_ setFrame:folderFrame];
809  NSSize newSize = NSMakeSize(windowWidth, 0.0);
810  [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0];
811  [self configureWindowLevel];
812  [window display];
813}
814
815// TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
816- (CGFloat)adjustButtonWidths {
817  CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
818  // Use the cell's size as the base for determining the desired width of the
819  // button rather than the button's current width. -[cell cellSize] always
820  // returns the 'optimum' size of the cell based on the cell's contents even
821  // if it's less than the current button size. Relying on the button size
822  // would result in buttons that could only get wider but we want to handle
823  // the case where the widest button gets removed from a folder menu.
824  for (BookmarkButton* button in buttons_.get())
825    width = std::max(width, [[button cell] cellSize].width);
826  width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
827  // Things look and feel more menu-like if all the buttons are the
828  // full width of the window, especially if there are submenus.
829  for (BookmarkButton* button in buttons_.get()) {
830    NSRect buttonFrame = [button frame];
831    buttonFrame.size.width = width;
832    [button setFrame:buttonFrame];
833  }
834  return width;
835}
836
837// Start a "scroll up" timer.
838- (void)beginScrollWindowUp {
839  [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
840}
841
842// Start a "scroll down" timer.
843- (void)beginScrollWindowDown {
844  [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
845}
846
847// End a scrolling timer.  Can be called excessively with no harm.
848- (void)endScroll {
849  if (scrollTimer_) {
850    [scrollTimer_ invalidate];
851    scrollTimer_ = nil;
852    verticalScrollDelta_ = 0;
853  }
854}
855
856- (int)indexOfButton:(BookmarkButton*)button {
857  if (button == nil)
858    return -1;
859  int index = [buttons_ indexOfObject:button];
860  return (index == NSNotFound) ? -1 : index;
861}
862
863- (BookmarkButton*)buttonAtIndex:(int)which {
864  if (which < 0 || which >= [self buttonCount])
865    return nil;
866  return [buttons_ objectAtIndex:which];
867}
868
869// Private, called by performOneScroll only.
870// If the button at index contains the mouse it will select it and return YES.
871// Otherwise returns NO.
872- (BOOL)selectButtonIfHoveredAtIndex:(int)index {
873  BookmarkButton *btn = [self buttonAtIndex:index];
874  if ([[btn cell] isMouseReallyInside]) {
875    buttonThatMouseIsIn_ = btn;
876    [self setSelectedButtonByIndex:index];
877    return YES;
878  }
879  return NO;
880}
881
882// Perform a single scroll of the specified amount.
883- (void)performOneScroll:(CGFloat)delta {
884  if (delta == 0.0)
885    return;
886  CGFloat finalDelta = [self determineFinalScrollDelta:delta];
887  if (finalDelta == 0.0)
888    return;
889  int index = [self indexOfButton:buttonThatMouseIsIn_];
890  // Check for a current mouse-initiated selection.
891  BOOL maintainHoverSelection =
892      (buttonThatMouseIsIn_ &&
893      [[buttonThatMouseIsIn_ cell] isMouseReallyInside] &&
894      selectedIndex_ != -1 &&
895      index == selectedIndex_);
896  NSRect windowFrame = [[self window] frame];
897  NSSize newSize = NSMakeSize(NSWidth(windowFrame), 0.0);
898  [self adjustWindowLeft:windowFrame.origin.x
899                    size:newSize
900             scrollingBy:finalDelta];
901  // We have now scrolled.
902  if (!maintainHoverSelection)
903    return;
904  // Is mouse still in the same hovered button?
905  if ([[buttonThatMouseIsIn_ cell] isMouseReallyInside])
906    return;
907  // The finalDelta scroll direction will tell us us whether to search up or
908  // down the buttons array for the newly hovered button.
909  if (finalDelta < 0.0) { // Scrolled up, so search backwards for new hover.
910    index--;
911    while (index >= 0) {
912      if ([self selectButtonIfHoveredAtIndex:index])
913        return;
914      index--;
915    }
916  } else { // Scrolled down, so search forward for new hovered button.
917    index++;
918    int btnMax = [self buttonCount];
919    while (index < btnMax) {
920      if ([self selectButtonIfHoveredAtIndex:index])
921        return;
922      index++;
923    }
924  }
925}
926
927- (CGFloat)determineFinalScrollDelta:(CGFloat)delta {
928  if ((delta > 0.0 && ![scrollUpArrowView_ isHidden]) ||
929      (delta < 0.0 && ![scrollDownArrowView_ isHidden])) {
930    NSWindow* window = [self window];
931    NSRect windowFrame = [window frame];
932    NSScreen* screen = [window screen];
933    NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
934    CGFloat scrollY = scrollPosition.y;
935    NSRect scrollerFrame = [scrollView_ frame];
936    CGFloat scrollerY = NSMinY(scrollerFrame);
937    NSRect visibleFrame = [visibleView_ frame];
938    CGFloat visibleY = NSMinY(visibleFrame);
939    CGFloat windowY = NSMinY(windowFrame);
940    CGFloat offset = scrollerY + visibleY + windowY;
941
942    if (delta > 0.0) {
943      // Scrolling up.
944      CGFloat minimumY = NSMinY([screen visibleFrame]) +
945                         bookmarks::kScrollWindowVerticalMargin;
946      CGFloat maxUpDelta = scrollY - offset + minimumY;
947      delta = MIN(delta, maxUpDelta);
948    } else {
949      // Scrolling down.
950      NSRect screenFrame =  [screen visibleFrame];
951      CGFloat topOfScreen = NSMaxY(screenFrame);
952      NSRect folderFrame = [folderView_ frame];
953      CGFloat folderHeight = NSHeight(folderFrame);
954      CGFloat folderTop = folderHeight - scrollY + offset;
955      CGFloat maxDownDelta =
956          topOfScreen - folderTop - bookmarks::kScrollWindowVerticalMargin;
957      delta = MAX(delta, maxDownDelta);
958    }
959  } else {
960    delta = 0.0;
961  }
962  return delta;
963}
964
965// Perform a scroll of the window on the screen.
966// Called by a timer when scrolling.
967- (void)performScroll:(NSTimer*)timer {
968  DCHECK(verticalScrollDelta_);
969  [self performOneScroll:verticalScrollDelta_];
970}
971
972
973// Add a timer to fire at a regular interval which scrolls the
974// window vertically |delta|.
975- (void)addScrollTimerWithDelta:(CGFloat)delta {
976  if (scrollTimer_ && verticalScrollDelta_ == delta)
977    return;
978  [self endScroll];
979  verticalScrollDelta_ = delta;
980  scrollTimer_ = [NSTimer timerWithTimeInterval:kBookmarkBarFolderScrollInterval
981                                         target:self
982                                       selector:@selector(performScroll:)
983                                       userInfo:nil
984                                        repeats:YES];
985
986  [[NSRunLoop mainRunLoop] addTimer:scrollTimer_ forMode:NSRunLoopCommonModes];
987}
988
989
990// Called as a result of our tracking area.  Warning: on the main
991// screen (of a single-screened machine), the minimum mouse y value is
992// 1, not 0.  Also, we do not get events when the mouse is above the
993// menubar (to be fixed by setting the proper window level; see
994// initializer).
995// Note [theEvent window] may not be our window, as we also get these messages
996// forwarded from BookmarkButton's mouse tracking loop.
997- (void)mouseMovedOrDragged:(NSEvent*)theEvent {
998  NSPoint eventScreenLocation =
999      [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]];
1000
1001  // Base hot spot calculations on the positions of the scroll arrow views.
1002  NSRect testRect = [scrollDownArrowView_ frame];
1003  NSPoint testPoint = [visibleView_ convertPoint:testRect.origin
1004                                                  toView:nil];
1005  testPoint = [[self window] convertBaseToScreen:testPoint];
1006  CGFloat closeToTopOfScreen = testPoint.y;
1007
1008  testRect = [scrollUpArrowView_ frame];
1009  testPoint = [visibleView_ convertPoint:testRect.origin toView:nil];
1010  testPoint = [[self window] convertBaseToScreen:testPoint];
1011  CGFloat closeToBottomOfScreen = testPoint.y + testRect.size.height;
1012  if (eventScreenLocation.y <= closeToBottomOfScreen &&
1013      ![scrollUpArrowView_ isHidden]) {
1014    [self beginScrollWindowUp];
1015  } else if (eventScreenLocation.y > closeToTopOfScreen &&
1016      ![scrollDownArrowView_ isHidden]) {
1017    [self beginScrollWindowDown];
1018  } else {
1019    [self endScroll];
1020  }
1021}
1022
1023- (void)mouseMoved:(NSEvent*)theEvent {
1024  [self mouseMovedOrDragged:theEvent];
1025}
1026
1027- (void)mouseDragged:(NSEvent*)theEvent {
1028  [self mouseMovedOrDragged:theEvent];
1029}
1030
1031- (void)mouseExited:(NSEvent*)theEvent {
1032  [self endScroll];
1033}
1034
1035// Add a tracking area so we know when the mouse is pinned to the top
1036// or bottom of the screen.  If that happens, and if the mouse
1037// position overlaps the window, scroll it.
1038- (void)addOrUpdateScrollTracking {
1039  [self removeScrollTracking];
1040  NSView* view = [[self window] contentView];
1041  scrollTrackingArea_.reset([[CrTrackingArea alloc]
1042                              initWithRect:[view bounds]
1043                                   options:(NSTrackingMouseMoved |
1044                                            NSTrackingMouseEnteredAndExited |
1045                                            NSTrackingActiveAlways |
1046                                            NSTrackingEnabledDuringMouseDrag
1047                                            )
1048                              proxiedOwner:self
1049                                  userInfo:nil]);
1050  [view addTrackingArea:scrollTrackingArea_.get()];
1051}
1052
1053// Remove the tracking area associated with scrolling.
1054- (void)removeScrollTracking {
1055  if (scrollTrackingArea_.get()) {
1056    [[[self window] contentView] removeTrackingArea:scrollTrackingArea_.get()];
1057    [scrollTrackingArea_.get() clearOwner];
1058  }
1059  scrollTrackingArea_.reset();
1060}
1061
1062// Close the old hover-open bookmark folder, and open a new one.  We
1063// do both in one step to allow for a delay in closing the old one.
1064// See comments above kDragHoverCloseDelay (bookmark_bar_controller.h)
1065// for more details.
1066- (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender {
1067  // If an old submenu exists, close it immediately.
1068  [self closeBookmarkFolder:sender];
1069
1070  // Open a new one if meaningful.
1071  if ([sender isFolder])
1072    [folderTarget_ openBookmarkFolderFromButton:sender];
1073}
1074
1075- (NSArray*)buttons {
1076  return buttons_.get();
1077}
1078
1079- (void)close {
1080  [folderController_ close];
1081  [super close];
1082}
1083
1084- (void)scrollWheel:(NSEvent *)theEvent {
1085  if (![scrollUpArrowView_ isHidden] || ![scrollDownArrowView_ isHidden]) {
1086    // We go negative since an NSScrollView has a flipped coordinate frame.
1087    CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY];
1088    [self performOneScroll:amt];
1089  }
1090}
1091
1092#pragma mark Actions Forwarded to Parent BookmarkBarController
1093
1094- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
1095  return [barController_ validateUserInterfaceItem:item];
1096}
1097
1098- (IBAction)openBookmark:(id)sender {
1099  [barController_ openBookmark:sender];
1100}
1101
1102- (IBAction)openBookmarkInNewForegroundTab:(id)sender {
1103  [barController_ openBookmarkInNewForegroundTab:sender];
1104}
1105
1106- (IBAction)openBookmarkInNewWindow:(id)sender {
1107  [barController_ openBookmarkInNewWindow:sender];
1108}
1109
1110- (IBAction)openBookmarkInIncognitoWindow:(id)sender {
1111  [barController_ openBookmarkInIncognitoWindow:sender];
1112}
1113
1114- (IBAction)editBookmark:(id)sender {
1115  [barController_ editBookmark:sender];
1116}
1117
1118- (IBAction)cutBookmark:(id)sender {
1119  [self closeBookmarkFolder:self];
1120  [barController_ cutBookmark:sender];
1121}
1122
1123- (IBAction)copyBookmark:(id)sender {
1124  [barController_ copyBookmark:sender];
1125}
1126
1127- (IBAction)pasteBookmark:(id)sender {
1128  [barController_ pasteBookmark:sender];
1129}
1130
1131- (IBAction)deleteBookmark:(id)sender {
1132  [self closeBookmarkFolder:self];
1133  [barController_ deleteBookmark:sender];
1134}
1135
1136- (IBAction)openAllBookmarks:(id)sender {
1137  [barController_ openAllBookmarks:sender];
1138}
1139
1140- (IBAction)openAllBookmarksNewWindow:(id)sender {
1141  [barController_ openAllBookmarksNewWindow:sender];
1142}
1143
1144- (IBAction)openAllBookmarksIncognitoWindow:(id)sender {
1145  [barController_ openAllBookmarksIncognitoWindow:sender];
1146}
1147
1148- (IBAction)addPage:(id)sender {
1149  [barController_ addPage:sender];
1150}
1151
1152- (IBAction)addFolder:(id)sender {
1153  [barController_ addFolder:sender];
1154}
1155
1156#pragma mark Drag & Drop
1157
1158// Find something like std::is_between<T>?  I can't believe one doesn't exist.
1159// http://crbug.com/35966
1160static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
1161  return ((value >= low) && (value <= high));
1162}
1163
1164// Return the proposed drop target for a hover open button, or nil if none.
1165//
1166// TODO(jrg): this is just like the version in
1167// bookmark_bar_controller.mm, but vertical instead of horizontal.
1168// Generalize to be axis independent then share code.
1169// http://crbug.com/35966
1170- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
1171  for (BookmarkButton* button in buttons_.get()) {
1172    // No early break -- makes no assumption about button ordering.
1173
1174    // Intentionally NOT using NSPointInRect() so that scrolling into
1175    // a submenu doesn't cause it to be closed.
1176    if (ValueInRangeInclusive(NSMinY([button frame]),
1177                              point.y,
1178                              NSMaxY([button frame]))) {
1179
1180      // Over a button but let's be a little more specific
1181      // (e.g. over the middle half).
1182      NSRect frame = [button frame];
1183      NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4);
1184      if (ValueInRangeInclusive(NSMinY(middleHalfOfButton),
1185                                point.y,
1186                                NSMaxY(middleHalfOfButton))) {
1187        // It makes no sense to drop on a non-folder; there is no hover.
1188        if (![button isFolder])
1189          return nil;
1190        // Got it!
1191        return button;
1192      } else {
1193        // Over a button but not over the middle half.
1194        return nil;
1195      }
1196    }
1197  }
1198  // Not hovering over a button.
1199  return nil;
1200}
1201
1202// TODO(jrg): again we have code dup, sort of, with
1203// bookmark_bar_controller.mm, but the axis is changed.  One minor
1204// difference is accomodation for the "empty" button (which may not
1205// exist in the future).
1206// http://crbug.com/35966
1207- (int)indexForDragToPoint:(NSPoint)point {
1208  // Identify which buttons we are between.  For now, assume a button
1209  // location is at the center point of its view, and that an exact
1210  // match means "place before".
1211  // TODO(jrg): revisit position info based on UI team feedback.
1212  // dropLocation is in bar local coordinates.
1213  // http://crbug.com/36276
1214  NSPoint dropLocation =
1215      [folderView_ convertPoint:point
1216                     fromView:[[self window] contentView]];
1217  BookmarkButton* buttonToTheTopOfDraggedButton = nil;
1218  // Buttons are laid out in this array from top to bottom (screen
1219  // wise), which means "biggest y" --> "smallest y".
1220  for (BookmarkButton* button in buttons_.get()) {
1221    CGFloat midpoint = NSMidY([button frame]);
1222    if (dropLocation.y > midpoint) {
1223      break;
1224    }
1225    buttonToTheTopOfDraggedButton = button;
1226  }
1227
1228  // TODO(jrg): On Windows, dropping onto (empty) highlights the
1229  // entire drop location and does not use an insertion point.
1230  // http://crbug.com/35967
1231  if (!buttonToTheTopOfDraggedButton) {
1232    // We are at the very top (we broke out of the loop on the first try).
1233    return 0;
1234  }
1235  if ([buttonToTheTopOfDraggedButton isEmpty]) {
1236    // There is a button but it's an empty placeholder.
1237    // Default to inserting on top of it.
1238    return 0;
1239  }
1240  const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton
1241                                       bookmarkNode];
1242  DCHECK(beforeNode);
1243  // Be careful if the number of buttons != number of nodes.
1244  return ((beforeNode->parent()->GetIndexOf(beforeNode) + 1) -
1245          [[parentButton_ cell] startingChildIndex]);
1246}
1247
1248// TODO(jrg): Yet more code dup.
1249// http://crbug.com/35966
1250- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
1251                  to:(NSPoint)point
1252                copy:(BOOL)copy {
1253  DCHECK(sourceNode);
1254
1255  // Drop destination.
1256  const BookmarkNode* destParent = NULL;
1257  int destIndex = 0;
1258
1259  // First check if we're dropping on a button.  If we have one, and
1260  // it's a folder, drop in it.
1261  BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1262  if ([button isFolder]) {
1263    destParent = [button bookmarkNode];
1264    // Drop it at the end.
1265    destIndex = [button bookmarkNode]->child_count();
1266  } else {
1267    // Else we're dropping somewhere in the folder, so find the right spot.
1268    destParent = [parentButton_ bookmarkNode];
1269    destIndex = [self indexForDragToPoint:point];
1270    // Be careful if the number of buttons != number of nodes.
1271    destIndex += [[parentButton_ cell] startingChildIndex];
1272  }
1273
1274  // Prevent cycles.
1275  BOOL wasCopiedOrMoved = NO;
1276  if (!destParent->HasAncestor(sourceNode)) {
1277    if (copy)
1278      [self bookmarkModel]->Copy(sourceNode, destParent, destIndex);
1279    else
1280      [self bookmarkModel]->Move(sourceNode, destParent, destIndex);
1281    wasCopiedOrMoved = YES;
1282    // Movement of a node triggers observers (like us) to rebuild the
1283    // bar so we don't have to do so explicitly.
1284  }
1285
1286  return wasCopiedOrMoved;
1287}
1288
1289// TODO(maf): Implement live drag & drop animation using this hook.
1290- (void)setDropInsertionPos:(CGFloat)where {
1291}
1292
1293// TODO(maf): Implement live drag & drop animation using this hook.
1294- (void)clearDropInsertionPos {
1295}
1296
1297#pragma mark NSWindowDelegate Functions
1298
1299- (void)windowWillClose:(NSNotification*)notification {
1300  // Also done by the dealloc method, but also doing it here is quicker and
1301  // more reliable.
1302  [parentButton_ forceButtonBorderToStayOnAlways:NO];
1303
1304  // If a "hover open" is pending when the bookmark bar folder is
1305  // closed, be sure it gets cancelled.
1306  [NSObject cancelPreviousPerformRequestsWithTarget:self];
1307
1308  [self endScroll];  // Just in case we were scrolling.
1309  [barController_ childFolderWillClose:self];
1310  [self closeBookmarkFolder:self];
1311  [self autorelease];
1312}
1313
1314#pragma mark BookmarkButtonDelegate Protocol
1315
1316- (void)fillPasteboard:(NSPasteboard*)pboard
1317       forDragOfButton:(BookmarkButton*)button {
1318  [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
1319
1320  // Close our folder menu and submenus since we know we're going to be dragged.
1321  [self closeBookmarkFolder:self];
1322}
1323
1324// Called from BookmarkButton.
1325// Unlike bookmark_bar_controller's version, we DO default to being enabled.
1326- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
1327  [[NSCursor arrowCursor] set];
1328
1329  buttonThatMouseIsIn_ = sender;
1330  [self setSelectedButtonByIndex:[self indexOfButton:sender]];
1331
1332  // Cancel a previous hover if needed.
1333  [NSObject cancelPreviousPerformRequestsWithTarget:self];
1334
1335  // If already opened, then we exited but re-entered the button
1336  // (without entering another button open), do nothing.
1337  if ([folderController_ parentButton] == sender)
1338    return;
1339
1340  [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:)
1341             withObject:sender
1342             afterDelay:bookmarks::kHoverOpenDelay
1343                inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
1344}
1345
1346// Called from the BookmarkButton
1347- (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
1348  if (buttonThatMouseIsIn_ == sender)
1349    buttonThatMouseIsIn_ = nil;
1350    [self setSelectedButtonByIndex:-1];
1351
1352  // Stop any timer about opening a new hover-open folder.
1353
1354  // Since a performSelector:withDelay: on self retains self, it is
1355  // possible that a cancelPreviousPerformRequestsWithTarget: reduces
1356  // the refcount to 0, releasing us.  That's a bad thing to do while
1357  // this object (or others it may own) is in the event chain.  Thus
1358  // we have a retain/autorelease.
1359  [self retain];
1360  [NSObject cancelPreviousPerformRequestsWithTarget:self];
1361  [self autorelease];
1362}
1363
1364- (NSWindow*)browserWindow {
1365  return [parentController_ browserWindow];
1366}
1367
1368- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
1369  return [barController_ canEditBookmarks] &&
1370         [barController_ canEditBookmark:[button bookmarkNode]];
1371}
1372
1373- (void)didDragBookmarkToTrash:(BookmarkButton*)button {
1374  [barController_ didDragBookmarkToTrash:button];
1375}
1376
1377- (void)bookmarkDragDidEnd:(BookmarkButton*)button
1378                 operation:(NSDragOperation)operation {
1379  [barController_ bookmarkDragDidEnd:button
1380                           operation:operation];
1381}
1382
1383
1384#pragma mark BookmarkButtonControllerProtocol
1385
1386// Recursively close all bookmark folders.
1387- (void)closeAllBookmarkFolders {
1388  // Closing the top level implicitly closes all children.
1389  [barController_ closeAllBookmarkFolders];
1390}
1391
1392// Close our bookmark folder (a sub-controller) if we have one.
1393- (void)closeBookmarkFolder:(id)sender {
1394  if (folderController_) {
1395    // Make this menu key, so key status doesn't go back to the browser
1396    // window when the submenu closes.
1397    [[self window] makeKeyWindow];
1398    [self setSubFolderGrowthToRight:YES];
1399    [[folderController_ window] close];
1400    folderController_ = nil;
1401  }
1402}
1403
1404- (BookmarkModel*)bookmarkModel {
1405  return [barController_ bookmarkModel];
1406}
1407
1408- (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
1409  return [barController_ draggingAllowed:info];
1410}
1411
1412// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1413// Most of the work (e.g. drop indicator) is taken care of in the
1414// folder_view.  Here we handle hover open issues for subfolders.
1415// Caution: there are subtle differences between this one and
1416// bookmark_bar_controller.mm's version.
1417- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
1418  NSPoint currentLocation = [info draggingLocation];
1419  BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
1420
1421  // Don't allow drops that would result in cycles.
1422  if (button) {
1423    NSData* data = [[info draggingPasteboard]
1424                    dataForType:kBookmarkButtonDragType];
1425    if (data && [info draggingSource]) {
1426      BookmarkButton* sourceButton = nil;
1427      [data getBytes:&sourceButton length:sizeof(sourceButton)];
1428      const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1429      const BookmarkNode* destNode = [button bookmarkNode];
1430      if (destNode->HasAncestor(sourceNode))
1431        button = nil;
1432    }
1433  }
1434  // Delegate handling of dragging over a button to the |hoverState_| member.
1435  return [hoverState_ draggingEnteredButton:button];
1436}
1437
1438- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info {
1439  return NSDragOperationMove;
1440}
1441
1442// Unlike bookmark_bar_controller, we need to keep track of dragging state.
1443// We also need to make sure we cancel the delayed hover close.
1444- (void)draggingExited:(id<NSDraggingInfo>)info {
1445  // NOT the same as a cancel --> we may have moved the mouse into the submenu.
1446  // Delegate handling of the hover button to the |hoverState_| member.
1447  [hoverState_ draggingExited];
1448}
1449
1450- (BOOL)dragShouldLockBarVisibility {
1451  return [parentController_ dragShouldLockBarVisibility];
1452}
1453
1454// TODO(jrg): ARGH more code dup.
1455// http://crbug.com/35966
1456- (BOOL)dragButton:(BookmarkButton*)sourceButton
1457                to:(NSPoint)point
1458              copy:(BOOL)copy {
1459  DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
1460  const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1461  return [self dragBookmark:sourceNode to:point copy:copy];
1462}
1463
1464// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1465// http://crbug.com/35966
1466- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
1467  BOOL dragged = NO;
1468  std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
1469  if (nodes.size()) {
1470    BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
1471    NSPoint dropPoint = [info draggingLocation];
1472    for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
1473         it != nodes.end(); ++it) {
1474      const BookmarkNode* sourceNode = *it;
1475      dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
1476    }
1477  }
1478  return dragged;
1479}
1480
1481// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1482// http://crbug.com/35966
1483- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
1484  std::vector<const BookmarkNode*> dragDataNodes;
1485  BookmarkNodeData dragData;
1486  if(dragData.ReadFromDragClipboard()) {
1487    BookmarkModel* bookmarkModel = [self bookmarkModel];
1488    Profile* profile = bookmarkModel->profile();
1489    std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile));
1490    dragDataNodes.assign(nodes.begin(), nodes.end());
1491  }
1492  return dragDataNodes;
1493}
1494
1495// Return YES if we should show the drop indicator, else NO.
1496// TODO(jrg): ARGH code dup!
1497// http://crbug.com/35966
1498- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
1499  return ![self buttonForDroppingOnAtPoint:point];
1500}
1501
1502// Button selection change code to support type to select and arrow key events.
1503#pragma mark Keyboard Support
1504
1505// Scroll the menu to show the selected button, if it's not already visible.
1506- (void)showSelectedButton {
1507  int bMaxIndex = [self buttonCount] - 1; // Max array index in button array.
1508
1509  // Is there a valid selected button?
1510  if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex)
1511    return;
1512
1513  // Is the menu scrollable anyway?
1514  if (![self canScrollUp] && ![self canScrollDown])
1515    return;
1516
1517  // Now check to see if we need to scroll, which way, and how far.
1518  CGFloat delta = 0.0;
1519  NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
1520  CGFloat itemBottom = (bMaxIndex - selectedIndex_) *
1521      bookmarks::kBookmarkFolderButtonHeight;
1522  CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight;
1523  CGFloat viewHeight = NSHeight([scrollView_  frame]);
1524
1525  if (scrollPoint.y > itemBottom) { // Need to scroll down.
1526    delta = scrollPoint.y - itemBottom;
1527  } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up.
1528    delta = -(itemTop - (scrollPoint.y + viewHeight));
1529  } else { // No need to scroll.
1530    return;
1531  }
1532
1533  [self performOneScroll:delta];
1534}
1535
1536// All changes to selectedness of buttons (aka fake menu items) ends up
1537// calling this method to actually flip the state of items.
1538// Needs to handle -1 as the invalid index (when nothing is selected) and
1539// greater than range values too.
1540- (void)setStateOfButtonByIndex:(int)index
1541                          state:(bool)state {
1542  if (index >= 0 && index < [self buttonCount])
1543    [[buttons_ objectAtIndex:index] highlight:state];
1544}
1545
1546// Selects the required button and deselects the previously selected one.
1547// An index of -1 means no selection.
1548- (void)setSelectedButtonByIndex:(int)index {
1549  if (index == selectedIndex_)
1550    return;
1551
1552  [self setStateOfButtonByIndex:selectedIndex_ state:NO];
1553  [self setStateOfButtonByIndex:index state:YES];
1554  selectedIndex_ = index;
1555
1556  [self showSelectedButton];
1557}
1558
1559- (void)clearInputText {
1560  [typedPrefix_ release];
1561  typedPrefix_ = nil;
1562}
1563
1564// Find the earliest item in the folder which has the target prefix.
1565// Returns nil if there is no prefix or there are no matches.
1566// These are in no particular order, and not particularly numerous, so linear
1567// search should be OK.
1568// -1 means no match.
1569- (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix {
1570  if ([prefix length] == 0) // Also handles nil.
1571    return -1;
1572  int maxButtons = [buttons_ count];
1573  NSString *lowercasePrefix = [prefix lowercaseString];
1574  for (int i = 0 ; i < maxButtons ; ++i) {
1575    BookmarkButton* button = [buttons_ objectAtIndex:i];
1576    if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix])
1577      return i;
1578  }
1579  return -1;
1580}
1581
1582- (void)setSelectedButtonByPrefix:(NSString*)prefix {
1583  [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]];
1584}
1585
1586- (void)selectPrevious {
1587  int newIndex;
1588  if (selectedIndex_ == 0)
1589    return;
1590  if (selectedIndex_ < 0)
1591    newIndex = [self buttonCount] -1;
1592  else
1593    newIndex = std::max(selectedIndex_ - 1, 0);
1594  [self setSelectedButtonByIndex:newIndex];
1595}
1596
1597- (void) selectNext {
1598  if (selectedIndex_ + 1 < [self buttonCount])
1599    [self setSelectedButtonByIndex:selectedIndex_ + 1];
1600}
1601
1602- (BOOL)handleInputText:(NSString*)newText {
1603  const unichar kUnicodeEscape = 0x001B;
1604  const unichar kUnicodeSpace = 0x0020;
1605
1606  // Event goes to the deepest nested open submenu.
1607  if (folderController_)
1608    return [folderController_ handleInputText:newText];
1609
1610  // Look for arrow keys or other function keys.
1611  if ([newText length] == 1) {
1612    // Get the 16-bit unicode char.
1613    unichar theChar = [newText characterAtIndex:0];
1614    switch (theChar) {
1615
1616      // Keys that trigger opening of the selection.
1617      case kUnicodeSpace: // Space.
1618      case NSNewlineCharacter:
1619      case NSCarriageReturnCharacter:
1620      case NSEnterCharacter:
1621        if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) {
1622          [self openBookmark:[buttons_ objectAtIndex:selectedIndex_]];
1623          return NO; // NO because the selection-handling code will close later.
1624        } else {
1625          return YES; // Triggering with no selection closes the menu.
1626        }
1627      // Keys that cancel and close the menu.
1628      case kUnicodeEscape:
1629      case NSDeleteCharacter:
1630      case NSBackspaceCharacter:
1631        [self clearInputText];
1632        return YES;
1633      // Keys that change selection directionally.
1634      case NSUpArrowFunctionKey:
1635        [self clearInputText];
1636        [self selectPrevious];
1637        return NO;
1638      case NSDownArrowFunctionKey:
1639        [self clearInputText];
1640        [self selectNext];
1641        return NO;
1642      // Keys that open and close submenus.
1643      case NSRightArrowFunctionKey: {
1644        BookmarkButton* btn = [self buttonAtIndex:selectedIndex_];
1645        if (btn && [btn isFolder]) {
1646          [self openBookmarkFolderFromButtonAndCloseOldOne:btn];
1647          [folderController_ selectNext];
1648        }
1649        [self clearInputText];
1650        return NO;
1651      }
1652      case NSLeftArrowFunctionKey:
1653        [self clearInputText];
1654        [parentController_ closeBookmarkFolder:self];
1655        return NO;
1656
1657      // Check for other keys that should close the menu.
1658      default: {
1659        if (theChar > NSUpArrowFunctionKey &&
1660            theChar <= NSModeSwitchFunctionKey) {
1661          [self clearInputText];
1662          return YES;
1663        }
1664        break;
1665      }
1666    }
1667  }
1668
1669  // It is a char or string worth adding to the type-select buffer.
1670  NSString *newString = (!typedPrefix_) ?
1671      newText : [typedPrefix_ stringByAppendingString:newText];
1672  [typedPrefix_ release];
1673  typedPrefix_ = [newString retain];
1674  [self setSelectedButtonByPrefix:typedPrefix_];
1675  return NO;
1676}
1677
1678// Return the y position for a drop indicator.
1679//
1680// TODO(jrg): again we have code dup, sort of, with
1681// bookmark_bar_controller.mm, but the axis is changed.
1682// http://crbug.com/35966
1683- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
1684  CGFloat y = 0;
1685  int destIndex = [self indexForDragToPoint:point];
1686  int numButtons = static_cast<int>([buttons_ count]);
1687
1688  // If it's a drop strictly between existing buttons or at the very beginning
1689  if (destIndex >= 0 && destIndex < numButtons) {
1690    // ... put the indicator right between the buttons.
1691    BookmarkButton* button =
1692        [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)];
1693    DCHECK(button);
1694    NSRect buttonFrame = [button frame];
1695    y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding;
1696
1697    // If it's a drop at the end (past the last button, if there are any) ...
1698  } else if (destIndex == numButtons) {
1699    // and if it's past the last button ...
1700    if (numButtons > 0) {
1701      // ... find the last button, and put the indicator below it.
1702      BookmarkButton* button =
1703          [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
1704      DCHECK(button);
1705      NSRect buttonFrame = [button frame];
1706      y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding;
1707
1708    }
1709  } else {
1710    NOTREACHED();
1711  }
1712
1713  return y;
1714}
1715
1716- (ui::ThemeProvider*)themeProvider {
1717  return [parentController_ themeProvider];
1718}
1719
1720- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
1721  // Do nothing.
1722}
1723
1724- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
1725  // Do nothing.
1726}
1727
1728- (BookmarkBarFolderController*)folderController {
1729  return folderController_;
1730}
1731
1732- (void)faviconLoadedForNode:(const BookmarkNode*)node {
1733  for (BookmarkButton* button in buttons_.get()) {
1734    if ([button bookmarkNode] == node) {
1735      [button setImage:[barController_ faviconForNode:node]];
1736      [button setNeedsDisplay:YES];
1737      return;
1738    }
1739  }
1740
1741  // Node was not in this menu, try submenu.
1742  if (folderController_)
1743    [folderController_ faviconLoadedForNode:node];
1744}
1745
1746// Add a new folder controller as triggered by the given folder button.
1747- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
1748  if (folderController_)
1749    [self closeBookmarkFolder:self];
1750
1751  // Folder controller, like many window controllers, owns itself.
1752  folderController_ =
1753      [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
1754                                               parentController:self
1755                                                  barController:barController_];
1756  [folderController_ showWindow:self];
1757}
1758
1759- (void)openAll:(const BookmarkNode*)node
1760    disposition:(WindowOpenDisposition)disposition {
1761  [barController_ openAll:node disposition:disposition];
1762}
1763
1764- (void)addButtonForNode:(const BookmarkNode*)node
1765                 atIndex:(NSInteger)buttonIndex {
1766  // Propose the frame for the new button. By default, this will be set to the
1767  // topmost button's frame (and there will always be one) offset upward in
1768  // anticipation of insertion.
1769  NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
1770  newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1771  // When adding a button to an empty folder we must remove the 'empty'
1772  // placeholder button. This can be detected by checking for a parent
1773  // child count of 1.
1774  const BookmarkNode* parentNode = node->parent();
1775  if (parentNode->child_count() == 1) {
1776    BookmarkButton* emptyButton = [buttons_ lastObject];
1777    newButtonFrame = [emptyButton frame];
1778    [emptyButton setDelegate:nil];
1779    [emptyButton removeFromSuperview];
1780    [buttons_ removeLastObject];
1781  }
1782
1783  if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
1784    buttonIndex = [buttons_ count];
1785
1786  // Offset upward by one button height all buttons above insertion location.
1787  BookmarkButton* button = nil;  // Remember so it can be de-highlighted.
1788  for (NSInteger i = 0; i < buttonIndex; ++i) {
1789    button = [buttons_ objectAtIndex:i];
1790    // Remember this location in case it's the last button being moved
1791    // which is where the new button will be located.
1792    newButtonFrame = [button frame];
1793    NSRect buttonFrame = [button frame];
1794    buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1795    [button setFrame:buttonFrame];
1796  }
1797  [[button cell] mouseExited:nil];  // De-highlight.
1798  BookmarkButton* newButton = [self makeButtonForNode:node
1799                                                frame:newButtonFrame];
1800  [buttons_ insertObject:newButton atIndex:buttonIndex];
1801  [folderView_ addSubview:newButton];
1802
1803  // Close any child folder(s) which may still be open.
1804  [self closeBookmarkFolder:self];
1805
1806  [self adjustWindowForButtonCount:[buttons_ count]];
1807}
1808
1809// More code which essentially duplicates that of BookmarkBarController.
1810// TODO(mrossetti,jrg): http://crbug.com/35966
1811- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
1812  DCHECK([urls count] == [titles count]);
1813  BOOL nodesWereAdded = NO;
1814  // Figure out where these new bookmarks nodes are to be added.
1815  BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1816  BookmarkModel* bookmarkModel = [self bookmarkModel];
1817  const BookmarkNode* destParent = NULL;
1818  int destIndex = 0;
1819  if ([button isFolder]) {
1820    destParent = [button bookmarkNode];
1821    // Drop it at the end.
1822    destIndex = [button bookmarkNode]->child_count();
1823  } else {
1824    // Else we're dropping somewhere in the folder, so find the right spot.
1825    destParent = [parentButton_ bookmarkNode];
1826    destIndex = [self indexForDragToPoint:point];
1827    // Be careful if the number of buttons != number of nodes.
1828    destIndex += [[parentButton_ cell] startingChildIndex];
1829  }
1830
1831  // Create and add the new bookmark nodes.
1832  size_t urlCount = [urls count];
1833  for (size_t i = 0; i < urlCount; ++i) {
1834    GURL gurl;
1835    const char* string = [[urls objectAtIndex:i] UTF8String];
1836    if (string)
1837      gurl = GURL(string);
1838    // We only expect to receive valid URLs.
1839    DCHECK(gurl.is_valid());
1840    if (gurl.is_valid()) {
1841      bookmarkModel->AddURL(destParent,
1842                            destIndex++,
1843                            base::SysNSStringToUTF16([titles objectAtIndex:i]),
1844                            gurl);
1845      nodesWereAdded = YES;
1846    }
1847  }
1848  return nodesWereAdded;
1849}
1850
1851- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
1852  if (fromIndex != toIndex) {
1853    if (toIndex == -1)
1854      toIndex = [buttons_ count];
1855    BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
1856    if (movedButton == buttonThatMouseIsIn_)
1857      buttonThatMouseIsIn_ = nil;
1858    [buttons_ removeObjectAtIndex:fromIndex];
1859    NSRect movedFrame = [movedButton frame];
1860    NSPoint toOrigin = movedFrame.origin;
1861    [movedButton setHidden:YES];
1862    if (fromIndex < toIndex) {
1863      BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
1864      toOrigin = [targetButton frame].origin;
1865      for (NSInteger i = fromIndex; i < toIndex; ++i) {
1866        BookmarkButton* button = [buttons_ objectAtIndex:i];
1867        NSRect frame = [button frame];
1868        frame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1869        [button setFrameOrigin:frame.origin];
1870      }
1871    } else {
1872      BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
1873      toOrigin = [targetButton frame].origin;
1874      for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
1875        BookmarkButton* button = [buttons_ objectAtIndex:i];
1876        NSRect buttonFrame = [button frame];
1877        buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1878        [button setFrameOrigin:buttonFrame.origin];
1879      }
1880    }
1881    [buttons_ insertObject:movedButton atIndex:toIndex];
1882    [movedButton setFrameOrigin:toOrigin];
1883    [movedButton setHidden:NO];
1884  }
1885}
1886
1887// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1888- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
1889  // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
1890  BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
1891  NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
1892
1893  // If a hover-open is pending, cancel it.
1894  if (oldButton == buttonThatMouseIsIn_) {
1895    [NSObject cancelPreviousPerformRequestsWithTarget:self];
1896    buttonThatMouseIsIn_ = nil;
1897  }
1898
1899  // Deleting a button causes rearrangement that enables us to lose a
1900  // mouse-exited event.  This problem doesn't appear to exist with
1901  // other keep-menu-open options (e.g. add folder).  Since the
1902  // showsBorderOnlyWhileMouseInside uses a tracking area, simple
1903  // tricks (e.g. sending an extra mouseExited: to the button) don't
1904  // fix the problem.
1905  // http://crbug.com/54324
1906  for (NSButton* button in buttons_.get()) {
1907    if ([button showsBorderOnlyWhileMouseInside]) {
1908      [button setShowsBorderOnlyWhileMouseInside:NO];
1909      [button setShowsBorderOnlyWhileMouseInside:YES];
1910    }
1911  }
1912
1913  [oldButton setDelegate:nil];
1914  [oldButton removeFromSuperview];
1915  [buttons_ removeObjectAtIndex:buttonIndex];
1916  for (NSInteger i = 0; i < buttonIndex; ++i) {
1917    BookmarkButton* button = [buttons_ objectAtIndex:i];
1918    NSRect buttonFrame = [button frame];
1919    buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1920    [button setFrame:buttonFrame];
1921  }
1922  // Search for and adjust submenus, if necessary.
1923  NSInteger buttonCount = [buttons_ count];
1924  if (buttonCount) {
1925    BookmarkButton* subButton = [folderController_ parentButton];
1926    for (NSInteger i = buttonIndex; i < buttonCount; ++i) {
1927      BookmarkButton* aButton = [buttons_ objectAtIndex:i];
1928      // If this button is showing its menu then we need to move the menu, too.
1929      if (aButton == subButton)
1930        [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0,
1931         bookmarks::kBookmarkBarHeight)];
1932    }
1933  } else {
1934    // If all nodes have been removed from this folder then add in the
1935    // 'empty' placeholder button.
1936    NSRect buttonFrame =
1937        NSMakeRect(0.0, 0.0, bookmarks::kDefaultBookmarkWidth,
1938                   bookmarks::kBookmarkFolderButtonHeight);
1939    BookmarkButton* button = [self makeButtonForNode:nil
1940                                               frame:buttonFrame];
1941    [buttons_ addObject:button];
1942    [folderView_ addSubview:button];
1943    buttonCount = 1;
1944  }
1945
1946  [self adjustWindowForButtonCount:buttonCount];
1947
1948  if (animate && !ignoreAnimations_)
1949    NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
1950                          NSZeroSize, nil, nil, nil);
1951}
1952
1953- (id<BookmarkButtonControllerProtocol>)controllerForNode:
1954    (const BookmarkNode*)node {
1955  // See if we are holding this node, otherwise see if it is in our
1956  // hierarchy of visible folder menus.
1957  if ([parentButton_ bookmarkNode] == node)
1958    return self;
1959  return [folderController_ controllerForNode:node];
1960}
1961
1962#pragma mark TestingAPI Only
1963
1964- (BOOL)canScrollUp {
1965  return ![scrollUpArrowView_ isHidden];
1966}
1967
1968- (BOOL)canScrollDown {
1969  return ![scrollDownArrowView_ isHidden];
1970}
1971
1972- (CGFloat)verticalScrollArrowHeight {
1973  return verticalScrollArrowHeight_;
1974}
1975
1976- (NSView*)visibleView {
1977  return visibleView_;
1978}
1979
1980- (NSScrollView*)scrollView {
1981  return scrollView_;
1982}
1983
1984- (NSView*)folderView {
1985  return folderView_;
1986}
1987
1988- (void)setIgnoreAnimations:(BOOL)ignore {
1989  ignoreAnimations_ = ignore;
1990}
1991
1992- (BookmarkButton*)buttonThatMouseIsIn {
1993  return buttonThatMouseIsIn_;
1994}
1995
1996@end  // BookmarkBarFolderController
1997