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