Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / bookmarks / bookmark_bar_folder_controller.mm
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
27 using bookmarks::BookmarkNodeData;
28 using bookmarks::kBookmarkBarMenuCornerRadius;
29
30 namespace {
31
32 // Frequency of the scrolling timer in seconds.
33 const 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.
37 const CGFloat kBookmarkBarFolderScrollAmount =
38     3 * bookmarks::kBookmarkFolderButtonHeight;
39
40 // Amount to scroll for each scroll wheel roll.
41 const 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.
48 struct 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
116 NSRect 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 or right. It is
530     // intentional that top level folders "push left" or "push right" slightly
531     // different than subfolders.
532     NSRect screenFrame = [screen_ visibleFrame];
533     // Test if window goes off-screen on the right side.
534     CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
535     if (spillOff > 0.0) {
536       newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
537                                     NSMinX(screenFrame));
538     } else if (newWindowTopLeft.x < NSMinX(screenFrame)) {
539       // For left side.
540       newWindowTopLeft.x = NSMinX(screenFrame);
541     }
542     // The menu looks bad when it is squeezed up against the bottom of the
543     // screen and ends up being only a few pixels tall. If it meets the
544     // threshold for this case, instead show the menu above the button.
545     CGFloat availableVerticalSpace = newWindowTopLeft.y -
546         (NSMinY(screenFrame) + bookmarks::kScrollWindowVerticalMargin);
547     if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
548         (windowHeight > availableVerticalSpace)) {
549       newWindowTopLeft.y = std::min(
550           newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
551           NSMaxY(screenFrame));
552     }
553   } else {
554     // Parent is a folder: expose as much as we can vertically; grow right/left.
555     newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
556     NSPoint topOfWindow = NSMakePoint(0,
557                                       NSMaxY([parentButton_ frame]) -
558                                           bookmarks::kBookmarkVerticalPadding);
559     topOfWindow = [[parentButton_ window]
560                    convertBaseToScreen:[[parentButton_ superview]
561                                         convertPoint:topOfWindow toView:nil]];
562     newWindowTopLeft.y = topOfWindow.y +
563                          2 * bookmarks::kBookmarkVerticalPadding;
564   }
565   return newWindowTopLeft;
566 }
567
568 // Set our window level to the right spot so we're above the menubar, dock, etc.
569 // Factored out so we can override/noop in a unit test.
570 - (void)configureWindowLevel {
571   [[self window] setLevel:NSPopUpMenuWindowLevel];
572 }
573
574 - (int)menuHeightForButtonCount:(int)buttonCount {
575   // This does not take into account any padding which may be required at the
576   // top and/or bottom of the window.
577   return (buttonCount * bookmarks::kBookmarkFolderButtonHeight) +
578       2 * bookmarks::kBookmarkVerticalPadding;
579 }
580
581 - (void)adjustWindowLeft:(CGFloat)windowLeft
582                     size:(NSSize)windowSize
583              scrollingBy:(CGFloat)scrollDelta {
584   // Callers of this function should make adjustments to the vertical
585   // attributes of the folder view only (height, scroll position).
586   // This function will then make appropriate layout adjustments in order
587   // to accommodate screen/dock margins, scroll-up and scroll-down arrow
588   // presentation, etc.
589   // The 4 views whose vertical height and origins may be adjusted
590   // by this function are:
591   //  1) window, 2) visible content view, 3) scroller view, 4) folder view.
592
593   LayoutMetrics layoutMetrics(windowLeft, windowSize, scrollDelta);
594   [self gatherMetrics:&layoutMetrics];
595   [self adjustMetrics:&layoutMetrics];
596   [self applyMetrics:&layoutMetrics];
597 }
598
599 - (void)gatherMetrics:(LayoutMetrics*)layoutMetrics {
600   LayoutMetrics& metrics(*layoutMetrics);
601   NSWindow* window = [self window];
602   metrics.windowFrame = [window frame];
603   metrics.visibleFrame = [visibleView_ frame];
604   metrics.scrollerFrame = [scrollView_ frame];
605   metrics.scrollPoint = [scrollView_ documentVisibleRect].origin;
606   metrics.scrollPoint.y -= metrics.scrollDelta;
607   metrics.couldScrollUp = ![scrollUpArrowView_ isHidden];
608   metrics.couldScrollDown = ![scrollDownArrowView_ isHidden];
609
610   metrics.deltaWindowHeight = 0.0;
611   metrics.deltaWindowY = 0.0;
612   metrics.deltaVisibleHeight = 0.0;
613   metrics.deltaVisibleY = 0.0;
614   metrics.deltaScrollerHeight = 0.0;
615   metrics.deltaScrollerY = 0.0;
616
617   metrics.minimumY = NSMinY([screen_ visibleFrame]) +
618                      bookmarks::kScrollWindowVerticalMargin;
619   metrics.screenBottomY = NSMinY([screen_ frame]);
620   metrics.oldWindowY = NSMinY(metrics.windowFrame);
621   metrics.folderY =
622       metrics.scrollerFrame.origin.y + metrics.visibleFrame.origin.y +
623       metrics.oldWindowY - metrics.scrollPoint.y;
624   metrics.folderTop = metrics.folderY + NSHeight([folderView_ frame]);
625 }
626
627 - (void)adjustMetrics:(LayoutMetrics*)layoutMetrics {
628   LayoutMetrics& metrics(*layoutMetrics);
629   CGFloat effectiveFolderY = metrics.folderY;
630   if (!metrics.couldScrollUp && !metrics.couldScrollDown)
631     effectiveFolderY -= metrics.windowSize.height;
632   metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
633   CGFloat maximumY =
634       NSMaxY([screen_ visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
635   metrics.canScrollDown = metrics.folderTop > maximumY;
636
637   // Accommodate changes in the bottom of the menu.
638   [self adjustMetricsForMenuBottomChanges:layoutMetrics];
639
640   // Accommodate changes in the top of the menu.
641   [self adjustMetricsForMenuTopChanges:layoutMetrics];
642
643   metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
644   metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
645   metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
646   metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
647   metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
648       metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
649   metrics.windowFrame.origin.y += metrics.deltaWindowY;
650   metrics.windowFrame.origin.x = metrics.windowLeft;
651   metrics.windowFrame.size.height += metrics.deltaWindowHeight;
652   metrics.windowFrame.size.width = metrics.windowSize.width;
653 }
654
655 - (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
656   LayoutMetrics& metrics(*layoutMetrics);
657   if (metrics.canScrollUp) {
658     if (!metrics.couldScrollUp) {
659       // Couldn't -> Can
660       metrics.deltaWindowY = metrics.screenBottomY - metrics.oldWindowY;
661       metrics.deltaWindowHeight = -metrics.deltaWindowY;
662       metrics.deltaVisibleY = metrics.minimumY - metrics.screenBottomY;
663       metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
664       metrics.deltaScrollerY = verticalScrollArrowHeight_;
665       metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
666       // Adjust the scroll delta if we've grown the window and it is
667       // now scroll-up-able, but don't adjust it if we've
668       // scrolled down and it wasn't scroll-up-able but now is.
669       if (metrics.canScrollDown == metrics.couldScrollDown) {
670         CGFloat deltaScroll = metrics.deltaWindowY - metrics.screenBottomY +
671                               metrics.deltaScrollerY + metrics.deltaVisibleY;
672         metrics.scrollPoint.y += deltaScroll + metrics.windowSize.height;
673       }
674     } else if (!metrics.canScrollDown && metrics.windowSize.height > 0.0) {
675       metrics.scrollPoint.y += metrics.windowSize.height;
676     }
677   } else {
678     if (metrics.couldScrollUp) {
679       // Could -> Can't
680       metrics.deltaWindowY = metrics.folderY - metrics.oldWindowY;
681       metrics.deltaWindowHeight = -metrics.deltaWindowY;
682       metrics.deltaVisibleY = -metrics.visibleFrame.origin.y;
683       metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
684       metrics.deltaScrollerY = -verticalScrollArrowHeight_;
685       metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
686       // We are no longer scroll-up-able so the scroll point drops to zero.
687       metrics.scrollPoint.y = 0.0;
688     } else {
689       // Couldn't -> Can't
690       // Check for menu height change by looking at the relative tops of the
691       // menu folder and the window folder, which previously would have been
692       // the same.
693       metrics.deltaWindowY = NSMaxY(metrics.windowFrame) - metrics.folderTop;
694       metrics.deltaWindowHeight = -metrics.deltaWindowY;
695     }
696   }
697 }
698
699 - (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics {
700   LayoutMetrics& metrics(*layoutMetrics);
701   if (metrics.canScrollDown == metrics.couldScrollDown) {
702     if (!metrics.canScrollDown) {
703       // Not scroll-down-able but the menu top has changed.
704       metrics.deltaWindowHeight += metrics.scrollDelta;
705     }
706   } else {
707     if (metrics.canScrollDown) {
708       // Couldn't -> Can
709       const CGFloat maximumY = NSMaxY([screen_ visibleFrame]);
710       metrics.deltaWindowHeight += (maximumY - NSMaxY(metrics.windowFrame));
711       metrics.deltaVisibleHeight -= bookmarks::kScrollWindowVerticalMargin;
712       metrics.deltaScrollerHeight -= verticalScrollArrowHeight_;
713     } else {
714       // Could -> Can't
715       metrics.deltaWindowHeight -= bookmarks::kScrollWindowVerticalMargin;
716       metrics.deltaVisibleHeight += bookmarks::kScrollWindowVerticalMargin;
717       metrics.deltaScrollerHeight += verticalScrollArrowHeight_;
718     }
719   }
720 }
721
722 - (void)applyMetrics:(LayoutMetrics*)layoutMetrics {
723   LayoutMetrics& metrics(*layoutMetrics);
724   // Hide or show the scroll arrows.
725   if (metrics.canScrollUp != metrics.couldScrollUp)
726     [scrollUpArrowView_ setHidden:metrics.couldScrollUp];
727   if (metrics.canScrollDown != metrics.couldScrollDown)
728     [scrollDownArrowView_ setHidden:metrics.couldScrollDown];
729
730   // Adjust the geometry. The order is important because of sizer dependencies.
731   [scrollView_ setFrame:metrics.scrollerFrame];
732   [visibleView_ setFrame:metrics.visibleFrame];
733   // This little bit of trickery handles the one special case where
734   // the window is now scroll-up-able _and_ going to be resized -- scroll
735   // first in order to prevent flashing.
736   if (metrics.preScroll)
737     [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
738
739   [[self window] setFrame:metrics.windowFrame display:YES];
740
741   // In all other cases we defer scrolling until the window has been resized
742   // in order to prevent flashing.
743   if (!metrics.preScroll)
744     [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
745
746   // TODO(maf) find a non-SPI way to do this.
747   // Hack. This is the only way I've found to get the tracking area cache
748   // to update properly during a mouse tracking loop.
749   // Without this, the item tracking-areas are wrong when using a scrollable
750   // menu with the mouse held down.
751   NSView *contentView = [[self window] contentView] ;
752   if ([contentView respondsToSelector:@selector(_updateTrackingAreas)])
753     [contentView _updateTrackingAreas];
754
755
756   if (metrics.canScrollUp != metrics.couldScrollUp ||
757       metrics.canScrollDown != metrics.couldScrollDown ||
758       metrics.scrollDelta != 0.0) {
759     if (metrics.canScrollUp || metrics.canScrollDown)
760       [self addOrUpdateScrollTracking];
761     else
762       [self removeScrollTracking];
763   }
764 }
765
766 - (void)adjustWindowForButtonCount:(NSUInteger)buttonCount {
767   NSRect folderFrame = [folderView_ frame];
768   CGFloat newMenuHeight =
769       (CGFloat)[self menuHeightForButtonCount:buttonCount];
770   CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
771   // If the height has changed then also change the origin, and adjust the
772   // scroll (if scrolling).
773   if ([self canScrollUp]) {
774     NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
775     scrollPoint.y += deltaMenuHeight;
776     [[scrollView_ documentView] scrollPoint:scrollPoint];
777   }
778   folderFrame.size.height += deltaMenuHeight;
779   [folderView_ setFrameSize:folderFrame.size];
780   CGFloat windowWidth = [self adjustButtonWidths] + padding_;
781   NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
782                                                   height:deltaMenuHeight];
783   CGFloat left = newWindowTopLeft.x;
784   NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
785   [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
786 }
787
788 // Determine window size and position.
789 // Create buttons for all our nodes.
790 // TODO(jrg): break up into more and smaller routines for easier unit testing.
791 - (void)configureWindow {
792   const BookmarkNode* node = [parentButton_ bookmarkNode];
793   DCHECK(node);
794   int startingIndex = [[parentButton_ cell] startingChildIndex];
795   DCHECK_LE(startingIndex, node->child_count());
796   // Must have at least 1 button (for "empty")
797   int buttons = std::max(node->child_count() - startingIndex, 1);
798
799   // Prelim height of the window.  We'll trim later as needed.
800   int height = [self menuHeightForButtonCount:buttons];
801   // We'll need this soon...
802   [self window];
803
804   // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
805   // http://crbug.com/35966
806   NSRect buttonsOuterFrame = GetFirstButtonFrameForHeight(height);
807
808   // TODO(jrg): combine with addNodesToButtonList: code from
809   // bookmark_bar_controller.mm (but use y offset)
810   // http://crbug.com/35966
811   if (node->empty()) {
812     // If no children we are the empty button.
813     BookmarkButton* button = [self makeButtonForNode:nil
814                                                frame:buttonsOuterFrame];
815     [buttons_ addObject:button];
816     [folderView_ addSubview:button];
817   } else {
818     for (int i = startingIndex; i < node->child_count(); ++i) {
819       const BookmarkNode* child = node->GetChild(i);
820       BookmarkButton* button = [self makeButtonForNode:child
821                                                  frame:buttonsOuterFrame];
822       [buttons_ addObject:button];
823       [folderView_ addSubview:button];
824       buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
825     }
826   }
827   [self layOutWindowWithHeight:height];
828 }
829
830 - (void)layOutWindowWithHeight:(CGFloat)height {
831   // Lay out the window by adjusting all button widths to be consistent, then
832   // base the window width on this ideal button width.
833   CGFloat buttonWidth = [self adjustButtonWidths];
834   CGFloat windowWidth = buttonWidth + padding_;
835   NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
836                                                   height:height];
837
838   // Make sure as much of a submenu is exposed (which otherwise would be a
839   // problem if the parent button is close to the bottom of the screen).
840   if ([parentController_ isKindOfClass:[self class]]) {
841     CGFloat minimumY = NSMinY([screen_ visibleFrame]) +
842                        bookmarks::kScrollWindowVerticalMargin +
843                        height;
844     newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
845   }
846
847   NSWindow* window = [self window];
848   NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
849                                   newWindowTopLeft.y - height,
850                                   windowWidth, height);
851   [window setFrame:windowFrame display:NO];
852
853   NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
854   [folderView_ setFrame:folderFrame];
855
856   // For some reason, when opening a "large" bookmark folder (containing 12 or
857   // more items) using the keyboard, the scroll view seems to want to be
858   // offset by default: [ http://crbug.com/101099 ].  Explicitly reseting the
859   // scroll position here is a bit hacky, but it does seem to work.
860   [[scrollView_ contentView] scrollToPoint:NSZeroPoint];
861
862   NSSize newSize = NSMakeSize(windowWidth, 0.0);
863   [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0];
864   [self configureWindowLevel];
865
866   [window display];
867 }
868
869 // TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
870 - (CGFloat)adjustButtonWidths {
871   CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
872   // Use the cell's size as the base for determining the desired width of the
873   // button rather than the button's current width. -[cell cellSize] always
874   // returns the 'optimum' size of the cell based on the cell's contents even
875   // if it's less than the current button size. Relying on the button size
876   // would result in buttons that could only get wider but we want to handle
877   // the case where the widest button gets removed from a folder menu.
878   for (BookmarkButton* button in buttons_.get())
879     width = std::max(width, [[button cell] cellSize].width);
880   width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
881   // Things look and feel more menu-like if all the buttons are the
882   // full width of the window, especially if there are submenus.
883   for (BookmarkButton* button in buttons_.get()) {
884     NSRect buttonFrame = [button frame];
885     buttonFrame.size.width = width;
886     [button setFrame:buttonFrame];
887   }
888   return width;
889 }
890
891 // Start a "scroll up" timer.
892 - (void)beginScrollWindowUp {
893   [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
894 }
895
896 // Start a "scroll down" timer.
897 - (void)beginScrollWindowDown {
898   [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
899 }
900
901 // End a scrolling timer.  Can be called excessively with no harm.
902 - (void)endScroll {
903   if (scrollTimer_) {
904     [scrollTimer_ invalidate];
905     scrollTimer_ = nil;
906     verticalScrollDelta_ = 0;
907   }
908 }
909
910 - (int)indexOfButton:(BookmarkButton*)button {
911   if (button == nil)
912     return -1;
913   NSInteger index = [buttons_ indexOfObject:button];
914   return (index == NSNotFound) ? -1 : index;
915 }
916
917 - (BookmarkButton*)buttonAtIndex:(int)which {
918   if (which < 0 || which >= [self buttonCount])
919     return nil;
920   return [buttons_ objectAtIndex:which];
921 }
922
923 // Private, called by performOneScroll only.
924 // If the button at index contains the mouse it will select it and return YES.
925 // Otherwise returns NO.
926 - (BOOL)selectButtonIfHoveredAtIndex:(int)index {
927   BookmarkButton* button = [self buttonAtIndex:index];
928   if ([[button cell] isMouseReallyInside]) {
929     buttonThatMouseIsIn_ = button;
930     [self setSelectedButtonByIndex:index];
931     return YES;
932   }
933   return NO;
934 }
935
936 // Perform a single scroll of the specified amount.
937 - (void)performOneScroll:(CGFloat)delta {
938   if (delta == 0.0)
939     return;
940   CGFloat finalDelta = [self determineFinalScrollDelta:delta];
941   if (finalDelta == 0.0)
942     return;
943   int index = [self indexOfButton:buttonThatMouseIsIn_];
944   // Check for a current mouse-initiated selection.
945   BOOL maintainHoverSelection =
946       (buttonThatMouseIsIn_ &&
947       [[buttonThatMouseIsIn_ cell] isMouseReallyInside] &&
948       selectedIndex_ != -1 &&
949       index == selectedIndex_);
950   NSRect windowFrame = [[self window] frame];
951   NSSize newSize = NSMakeSize(NSWidth(windowFrame), 0.0);
952   [self adjustWindowLeft:windowFrame.origin.x
953                     size:newSize
954              scrollingBy:finalDelta];
955   // We have now scrolled.
956   if (!maintainHoverSelection)
957     return;
958   // Is mouse still in the same hovered button?
959   if ([[buttonThatMouseIsIn_ cell] isMouseReallyInside])
960     return;
961   // The finalDelta scroll direction will tell us us whether to search up or
962   // down the buttons array for the newly hovered button.
963   if (finalDelta < 0.0) { // Scrolled up, so search backwards for new hover.
964     index--;
965     while (index >= 0) {
966       if ([self selectButtonIfHoveredAtIndex:index])
967         return;
968       index--;
969     }
970   } else { // Scrolled down, so search forward for new hovered button.
971     index++;
972     int btnMax = [self buttonCount];
973     while (index < btnMax) {
974       if ([self selectButtonIfHoveredAtIndex:index])
975         return;
976       index++;
977     }
978   }
979 }
980
981 - (CGFloat)determineFinalScrollDelta:(CGFloat)delta {
982   if ((delta > 0.0 && ![scrollUpArrowView_ isHidden]) ||
983       (delta < 0.0 && ![scrollDownArrowView_ isHidden])) {
984     NSWindow* window = [self window];
985     NSRect windowFrame = [window frame];
986     NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
987     CGFloat scrollY = scrollPosition.y;
988     NSRect scrollerFrame = [scrollView_ frame];
989     CGFloat scrollerY = NSMinY(scrollerFrame);
990     NSRect visibleFrame = [visibleView_ frame];
991     CGFloat visibleY = NSMinY(visibleFrame);
992     CGFloat windowY = NSMinY(windowFrame);
993     CGFloat offset = scrollerY + visibleY + windowY;
994
995     if (delta > 0.0) {
996       // Scrolling up.
997       CGFloat minimumY = NSMinY([screen_ visibleFrame]) +
998                          bookmarks::kScrollWindowVerticalMargin;
999       CGFloat maxUpDelta = scrollY - offset + minimumY;
1000       delta = MIN(delta, maxUpDelta);
1001     } else {
1002       // Scrolling down.
1003       NSRect screenFrame =  [screen_ visibleFrame];
1004       CGFloat topOfScreen = NSMaxY(screenFrame);
1005       NSRect folderFrame = [folderView_ frame];
1006       CGFloat folderHeight = NSHeight(folderFrame);
1007       CGFloat folderTop = folderHeight - scrollY + offset;
1008       CGFloat maxDownDelta =
1009           topOfScreen - folderTop - bookmarks::kScrollWindowVerticalMargin;
1010       delta = MAX(delta, maxDownDelta);
1011     }
1012   } else {
1013     delta = 0.0;
1014   }
1015   return delta;
1016 }
1017
1018 // Perform a scroll of the window on the screen.
1019 // Called by a timer when scrolling.
1020 - (void)performScroll:(NSTimer*)timer {
1021   DCHECK(verticalScrollDelta_);
1022   [self performOneScroll:verticalScrollDelta_];
1023 }
1024
1025
1026 // Add a timer to fire at a regular interval which scrolls the
1027 // window vertically |delta|.
1028 - (void)addScrollTimerWithDelta:(CGFloat)delta {
1029   if (scrollTimer_ && verticalScrollDelta_ == delta)
1030     return;
1031   [self endScroll];
1032   verticalScrollDelta_ = delta;
1033   scrollTimer_ = [NSTimer timerWithTimeInterval:kBookmarkBarFolderScrollInterval
1034                                          target:self
1035                                        selector:@selector(performScroll:)
1036                                        userInfo:nil
1037                                         repeats:YES];
1038
1039   [[NSRunLoop mainRunLoop] addTimer:scrollTimer_ forMode:NSRunLoopCommonModes];
1040 }
1041
1042
1043 // Called as a result of our tracking area.  Warning: on the main
1044 // screen (of a single-screened machine), the minimum mouse y value is
1045 // 1, not 0.  Also, we do not get events when the mouse is above the
1046 // menubar (to be fixed by setting the proper window level; see
1047 // initializer).
1048 // Note [theEvent window] may not be our window, as we also get these messages
1049 // forwarded from BookmarkButton's mouse tracking loop.
1050 - (void)mouseMovedOrDragged:(NSEvent*)theEvent {
1051   NSPoint eventScreenLocation =
1052       [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]];
1053
1054   // Base hot spot calculations on the positions of the scroll arrow views.
1055   NSRect testRect = [scrollDownArrowView_ frame];
1056   NSPoint testPoint = [visibleView_ convertPoint:testRect.origin
1057                                                   toView:nil];
1058   testPoint = [[self window] convertBaseToScreen:testPoint];
1059   CGFloat closeToTopOfScreen = testPoint.y;
1060
1061   testRect = [scrollUpArrowView_ frame];
1062   testPoint = [visibleView_ convertPoint:testRect.origin toView:nil];
1063   testPoint = [[self window] convertBaseToScreen:testPoint];
1064   CGFloat closeToBottomOfScreen = testPoint.y + testRect.size.height;
1065   if (eventScreenLocation.y <= closeToBottomOfScreen &&
1066       ![scrollUpArrowView_ isHidden]) {
1067     [self beginScrollWindowUp];
1068   } else if (eventScreenLocation.y > closeToTopOfScreen &&
1069       ![scrollDownArrowView_ isHidden]) {
1070     [self beginScrollWindowDown];
1071   } else {
1072     [self endScroll];
1073   }
1074 }
1075
1076 - (void)mouseMoved:(NSEvent*)theEvent {
1077   [self mouseMovedOrDragged:theEvent];
1078 }
1079
1080 - (void)mouseDragged:(NSEvent*)theEvent {
1081   [self mouseMovedOrDragged:theEvent];
1082 }
1083
1084 - (void)mouseExited:(NSEvent*)theEvent {
1085   [self endScroll];
1086 }
1087
1088 // Add a tracking area so we know when the mouse is pinned to the top
1089 // or bottom of the screen.  If that happens, and if the mouse
1090 // position overlaps the window, scroll it.
1091 - (void)addOrUpdateScrollTracking {
1092   [self removeScrollTracking];
1093   NSView* view = [[self window] contentView];
1094   scrollTrackingArea_.reset([[CrTrackingArea alloc]
1095                               initWithRect:[view bounds]
1096                                    options:(NSTrackingMouseMoved |
1097                                             NSTrackingMouseEnteredAndExited |
1098                                             NSTrackingActiveAlways |
1099                                             NSTrackingEnabledDuringMouseDrag
1100                                             )
1101                                      owner:self
1102                                   userInfo:nil]);
1103   [view addTrackingArea:scrollTrackingArea_.get()];
1104 }
1105
1106 // Remove the tracking area associated with scrolling.
1107 - (void)removeScrollTracking {
1108   if (scrollTrackingArea_.get()) {
1109     [[[self window] contentView] removeTrackingArea:scrollTrackingArea_.get()];
1110     [scrollTrackingArea_.get() clearOwner];
1111   }
1112   scrollTrackingArea_.reset();
1113 }
1114
1115 // Close the old hover-open bookmark folder, and open a new one.  We
1116 // do both in one step to allow for a delay in closing the old one.
1117 // See comments above kDragHoverCloseDelay (bookmark_bar_controller.h)
1118 // for more details.
1119 - (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender {
1120   // Ignore if sender button is in a window that's just been hidden - that
1121   // would leave us with an orphaned menu. BUG 69002
1122   if ([[sender window] isVisible] != YES)
1123     return;
1124   // If an old submenu exists, close it immediately.
1125   [self closeBookmarkFolder:sender];
1126
1127   // Open a new one if meaningful.
1128   if ([sender isFolder])
1129     [folderTarget_ openBookmarkFolderFromButton:sender];
1130 }
1131
1132 - (NSArray*)buttons {
1133   return buttons_.get();
1134 }
1135
1136 - (void)close {
1137   [folderController_ close];
1138   [super close];
1139 }
1140
1141 - (void)scrollWheel:(NSEvent *)theEvent {
1142   if (![scrollUpArrowView_ isHidden] || ![scrollDownArrowView_ isHidden]) {
1143     // We go negative since an NSScrollView has a flipped coordinate frame.
1144     CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY];
1145     [self performOneScroll:amt];
1146   }
1147 }
1148
1149 #pragma mark Drag & Drop
1150
1151 // Find something like std::is_between<T>?  I can't believe one doesn't exist.
1152 // http://crbug.com/35966
1153 static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
1154   return ((value >= low) && (value <= high));
1155 }
1156
1157 // Return the proposed drop target for a hover open button, or nil if none.
1158 //
1159 // TODO(jrg): this is just like the version in
1160 // bookmark_bar_controller.mm, but vertical instead of horizontal.
1161 // Generalize to be axis independent then share code.
1162 // http://crbug.com/35966
1163 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
1164   NSPoint localPoint = [folderView_ convertPoint:point fromView:nil];
1165   for (BookmarkButton* button in buttons_.get()) {
1166     // No early break -- makes no assumption about button ordering.
1167
1168     // Intentionally NOT using NSPointInRect() so that scrolling into
1169     // a submenu doesn't cause it to be closed.
1170     if (ValueInRangeInclusive(NSMinY([button frame]),
1171                               localPoint.y,
1172                               NSMaxY([button frame]))) {
1173
1174       // Over a button but let's be a little more specific
1175       // (e.g. over the middle half).
1176       NSRect frame = [button frame];
1177       NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4);
1178       if (ValueInRangeInclusive(NSMinY(middleHalfOfButton),
1179                                 localPoint.y,
1180                                 NSMaxY(middleHalfOfButton))) {
1181         // It makes no sense to drop on a non-folder; there is no hover.
1182         if (![button isFolder])
1183           return nil;
1184         // Got it!
1185         return button;
1186       } else {
1187         // Over a button but not over the middle half.
1188         return nil;
1189       }
1190     }
1191   }
1192   // Not hovering over a button.
1193   return nil;
1194 }
1195
1196 // TODO(jrg): again we have code dup, sort of, with
1197 // bookmark_bar_controller.mm, but the axis is changed.  One minor
1198 // difference is accomodation for the "empty" button (which may not
1199 // exist in the future).
1200 // http://crbug.com/35966
1201 - (int)indexForDragToPoint:(NSPoint)point {
1202   // Identify which buttons we are between.  For now, assume a button
1203   // location is at the center point of its view, and that an exact
1204   // match means "place before".
1205   // TODO(jrg): revisit position info based on UI team feedback.
1206   // dropLocation is in bar local coordinates.
1207   // http://crbug.com/36276
1208   NSPoint dropLocation =
1209       [folderView_ convertPoint:point
1210                        fromView:[[self window] contentView]];
1211   BookmarkButton* buttonToTheTopOfDraggedButton = nil;
1212   // Buttons are laid out in this array from top to bottom (screen
1213   // wise), which means "biggest y" --> "smallest y".
1214   for (BookmarkButton* button in buttons_.get()) {
1215     CGFloat midpoint = NSMidY([button frame]);
1216     if (dropLocation.y > midpoint) {
1217       break;
1218     }
1219     buttonToTheTopOfDraggedButton = button;
1220   }
1221
1222   // TODO(jrg): On Windows, dropping onto (empty) highlights the
1223   // entire drop location and does not use an insertion point.
1224   // http://crbug.com/35967
1225   if (!buttonToTheTopOfDraggedButton) {
1226     // We are at the very top (we broke out of the loop on the first try).
1227     return 0;
1228   }
1229   if ([buttonToTheTopOfDraggedButton isEmpty]) {
1230     // There is a button but it's an empty placeholder.
1231     // Default to inserting on top of it.
1232     return 0;
1233   }
1234   const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton
1235                                        bookmarkNode];
1236   DCHECK(beforeNode);
1237   // Be careful if the number of buttons != number of nodes.
1238   return ((beforeNode->parent()->GetIndexOf(beforeNode) + 1) -
1239           [[parentButton_ cell] startingChildIndex]);
1240 }
1241
1242 // TODO(jrg): Yet more code dup.
1243 // http://crbug.com/35966
1244 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
1245                   to:(NSPoint)point
1246                 copy:(BOOL)copy {
1247   DCHECK(sourceNode);
1248
1249   // Drop destination.
1250   const BookmarkNode* destParent = NULL;
1251   int destIndex = 0;
1252
1253   // First check if we're dropping on a button.  If we have one, and
1254   // it's a folder, drop in it.
1255   BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1256   if ([button isFolder]) {
1257     destParent = [button bookmarkNode];
1258     // Drop it at the end.
1259     destIndex = [button bookmarkNode]->child_count();
1260   } else {
1261     // Else we're dropping somewhere in the folder, so find the right spot.
1262     destParent = [parentButton_ bookmarkNode];
1263     destIndex = [self indexForDragToPoint:point];
1264     // Be careful if the number of buttons != number of nodes.
1265     destIndex += [[parentButton_ cell] startingChildIndex];
1266   }
1267
1268   ChromeBookmarkClient* client =
1269       ChromeBookmarkClientFactory::GetForProfile(profile_);
1270   if (!client->CanBeEditedByUser(destParent))
1271     return NO;
1272   if (!client->CanBeEditedByUser(sourceNode))
1273     copy = YES;
1274
1275   // Prevent cycles.
1276   BOOL wasCopiedOrMoved = NO;
1277   if (!destParent->HasAncestor(sourceNode)) {
1278     if (copy)
1279       [self bookmarkModel]->Copy(sourceNode, destParent, destIndex);
1280     else
1281       [self bookmarkModel]->Move(sourceNode, destParent, destIndex);
1282     wasCopiedOrMoved = YES;
1283     // Movement of a node triggers observers (like us) to rebuild the
1284     // bar so we don't have to do so explicitly.
1285   }
1286
1287   return wasCopiedOrMoved;
1288 }
1289
1290 // TODO(maf): Implement live drag & drop animation using this hook.
1291 - (void)setDropInsertionPos:(CGFloat)where {
1292 }
1293
1294 // TODO(maf): Implement live drag & drop animation using this hook.
1295 - (void)clearDropInsertionPos {
1296 }
1297
1298 #pragma mark NSWindowDelegate Functions
1299
1300 - (void)windowWillClose:(NSNotification*)notification {
1301   // Also done by the dealloc method, but also doing it here is quicker and
1302   // more reliable.
1303   [parentButton_ forceButtonBorderToStayOnAlways:NO];
1304
1305   // If a "hover open" is pending when the bookmark bar folder is
1306   // closed, be sure it gets cancelled.
1307   [NSObject cancelPreviousPerformRequestsWithTarget:self];
1308
1309   [self endScroll];  // Just in case we were scrolling.
1310   [barController_ childFolderWillClose:self];
1311   [self closeBookmarkFolder:self];
1312   [self autorelease];
1313 }
1314
1315 #pragma mark BookmarkButtonDelegate Protocol
1316
1317 - (void)fillPasteboard:(NSPasteboard*)pboard
1318        forDragOfButton:(BookmarkButton*)button {
1319   [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
1320
1321   // Close our folder menu and submenus since we know we're going to be dragged.
1322   [self closeBookmarkFolder:self];
1323 }
1324
1325 // Called from BookmarkButton.
1326 // Unlike bookmark_bar_controller's version, we DO default to being enabled.
1327 - (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
1328   [[NSCursor arrowCursor] set];
1329
1330   buttonThatMouseIsIn_ = sender;
1331   [self setSelectedButtonByIndex:[self indexOfButton:sender]];
1332
1333   // Cancel a previous hover if needed.
1334   [NSObject cancelPreviousPerformRequestsWithTarget:self];
1335
1336   // If already opened, then we exited but re-entered the button
1337   // (without entering another button open), do nothing.
1338   if ([folderController_ parentButton] == sender)
1339     return;
1340
1341   // If right click was done immediately on entering a button, then open the
1342   // folder without delay so that context menu appears over the folder menu.
1343   if ([event type] == NSRightMouseDown)
1344     [self openBookmarkFolderFromButtonAndCloseOldOne:sender];
1345   else
1346     [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:)
1347                withObject:sender
1348                afterDelay:bookmarks::kHoverOpenDelay
1349                   inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
1350 }
1351
1352 // Called from the BookmarkButton
1353 - (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
1354   if (buttonThatMouseIsIn_ == sender)
1355     buttonThatMouseIsIn_ = nil;
1356     [self setSelectedButtonByIndex:-1];
1357
1358   // Stop any timer about opening a new hover-open folder.
1359
1360   // Since a performSelector:withDelay: on self retains self, it is
1361   // possible that a cancelPreviousPerformRequestsWithTarget: reduces
1362   // the refcount to 0, releasing us.  That's a bad thing to do while
1363   // this object (or others it may own) is in the event chain.  Thus
1364   // we have a retain/autorelease.
1365   [self retain];
1366   [NSObject cancelPreviousPerformRequestsWithTarget:self];
1367   [self autorelease];
1368 }
1369
1370 - (NSWindow*)browserWindow {
1371   return [barController_ browserWindow];
1372 }
1373
1374 - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
1375   return [barController_ canEditBookmarks] &&
1376          [barController_ canEditBookmark:[button bookmarkNode]];
1377 }
1378
1379 - (void)didDragBookmarkToTrash:(BookmarkButton*)button {
1380   [barController_ didDragBookmarkToTrash:button];
1381 }
1382
1383 - (void)bookmarkDragDidEnd:(BookmarkButton*)button
1384                  operation:(NSDragOperation)operation {
1385   [barController_ bookmarkDragDidEnd:button
1386                            operation:operation];
1387 }
1388
1389
1390 #pragma mark BookmarkButtonControllerProtocol
1391
1392 // Recursively close all bookmark folders.
1393 - (void)closeAllBookmarkFolders {
1394   // Closing the top level implicitly closes all children.
1395   [barController_ closeAllBookmarkFolders];
1396 }
1397
1398 // Close our bookmark folder (a sub-controller) if we have one.
1399 - (void)closeBookmarkFolder:(id)sender {
1400   if (folderController_) {
1401     // Make this menu key, so key status doesn't go back to the browser
1402     // window when the submenu closes.
1403     [[self window] makeKeyWindow];
1404     [self setSubFolderGrowthToRight:YES];
1405     [[folderController_ window] close];
1406     folderController_ = nil;
1407   }
1408 }
1409
1410 - (BookmarkModel*)bookmarkModel {
1411   return [barController_ bookmarkModel];
1412 }
1413
1414 - (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
1415   return [barController_ draggingAllowed:info];
1416 }
1417
1418 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1419 // Most of the work (e.g. drop indicator) is taken care of in the
1420 // folder_view.  Here we handle hover open issues for subfolders.
1421 // Caution: there are subtle differences between this one and
1422 // bookmark_bar_controller.mm's version.
1423 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
1424   NSPoint currentLocation = [info draggingLocation];
1425   BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
1426
1427   // Don't allow drops that would result in cycles.
1428   if (button) {
1429     NSData* data = [[info draggingPasteboard]
1430                     dataForType:kBookmarkButtonDragType];
1431     if (data && [info draggingSource]) {
1432       BookmarkButton* sourceButton = nil;
1433       [data getBytes:&sourceButton length:sizeof(sourceButton)];
1434       const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1435       const BookmarkNode* destNode = [button bookmarkNode];
1436       if (destNode->HasAncestor(sourceNode))
1437         button = nil;
1438     }
1439   }
1440   // Delegate handling of dragging over a button to the |hoverState_| member.
1441   return [hoverState_ draggingEnteredButton:button];
1442 }
1443
1444 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info {
1445   return NSDragOperationMove;
1446 }
1447
1448 // Unlike bookmark_bar_controller, we need to keep track of dragging state.
1449 // We also need to make sure we cancel the delayed hover close.
1450 - (void)draggingExited:(id<NSDraggingInfo>)info {
1451   // NOT the same as a cancel --> we may have moved the mouse into the submenu.
1452   // Delegate handling of the hover button to the |hoverState_| member.
1453   [hoverState_ draggingExited];
1454 }
1455
1456 - (BOOL)dragShouldLockBarVisibility {
1457   return [parentController_ dragShouldLockBarVisibility];
1458 }
1459
1460 // TODO(jrg): ARGH more code dup.
1461 // http://crbug.com/35966
1462 - (BOOL)dragButton:(BookmarkButton*)sourceButton
1463                 to:(NSPoint)point
1464               copy:(BOOL)copy {
1465   DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
1466   const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1467   return [self dragBookmark:sourceNode to:point copy:copy];
1468 }
1469
1470 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1471 // http://crbug.com/35966
1472 - (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
1473   BOOL dragged = NO;
1474   std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
1475   if (nodes.size()) {
1476     BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
1477     NSPoint dropPoint = [info draggingLocation];
1478     for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
1479          it != nodes.end(); ++it) {
1480       const BookmarkNode* sourceNode = *it;
1481       dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
1482     }
1483   }
1484   return dragged;
1485 }
1486
1487 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1488 // http://crbug.com/35966
1489 - (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
1490   std::vector<const BookmarkNode*> dragDataNodes;
1491   BookmarkNodeData dragData;
1492   if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) {
1493     BookmarkModel* bookmarkModel = [self bookmarkModel];
1494     std::vector<const BookmarkNode*> nodes(
1495         dragData.GetNodes(bookmarkModel, profile_->GetPath()));
1496     dragDataNodes.assign(nodes.begin(), nodes.end());
1497   }
1498   return dragDataNodes;
1499 }
1500
1501 // Return YES if we should show the drop indicator, else NO.
1502 // TODO(jrg): ARGH code dup!
1503 // http://crbug.com/35966
1504 - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
1505   return ![self buttonForDroppingOnAtPoint:point];
1506 }
1507
1508 // Button selection change code to support type to select and arrow key events.
1509 #pragma mark Keyboard Support
1510
1511 // Scroll the menu to show the selected button, if it's not already visible.
1512 - (void)showSelectedButton {
1513   int bMaxIndex = [self buttonCount] - 1; // Max array index in button array.
1514
1515   // Is there a valid selected button?
1516   if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex)
1517     return;
1518
1519   // Is the menu scrollable anyway?
1520   if (![self canScrollUp] && ![self canScrollDown])
1521     return;
1522
1523   // Now check to see if we need to scroll, which way, and how far.
1524   CGFloat delta = 0.0;
1525   NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
1526   CGFloat itemBottom = (bMaxIndex - selectedIndex_) *
1527       bookmarks::kBookmarkFolderButtonHeight;
1528   CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight;
1529   CGFloat viewHeight = NSHeight([scrollView_  frame]);
1530
1531   if (scrollPoint.y > itemBottom) { // Need to scroll down.
1532     delta = scrollPoint.y - itemBottom;
1533   } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up.
1534     delta = -(itemTop - (scrollPoint.y + viewHeight));
1535   } else { // No need to scroll.
1536     return;
1537   }
1538
1539   [self performOneScroll:delta];
1540 }
1541
1542 // All changes to selectedness of buttons (aka fake menu items) ends up
1543 // calling this method to actually flip the state of items.
1544 // Needs to handle -1 as the invalid index (when nothing is selected) and
1545 // greater than range values too.
1546 - (void)setStateOfButtonByIndex:(int)index
1547                           state:(bool)state {
1548   if (index >= 0 && index < [self buttonCount])
1549     [[buttons_ objectAtIndex:index] highlight:state];
1550 }
1551
1552 // Selects the required button and deselects the previously selected one.
1553 // An index of -1 means no selection.
1554 - (void)setSelectedButtonByIndex:(int)index {
1555   if (index == selectedIndex_)
1556     return;
1557
1558   [self setStateOfButtonByIndex:selectedIndex_ state:NO];
1559   [self setStateOfButtonByIndex:index state:YES];
1560   selectedIndex_ = index;
1561
1562   [self showSelectedButton];
1563 }
1564
1565 - (void)clearInputText {
1566   [typedPrefix_ release];
1567   typedPrefix_ = nil;
1568 }
1569
1570 // Find the earliest item in the folder which has the target prefix.
1571 // Returns nil if there is no prefix or there are no matches.
1572 // These are in no particular order, and not particularly numerous, so linear
1573 // search should be OK.
1574 // -1 means no match.
1575 - (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix {
1576   if ([prefix length] == 0) // Also handles nil.
1577     return -1;
1578   int maxButtons = [buttons_ count];
1579   NSString* lowercasePrefix = [prefix lowercaseString];
1580   for (int i = 0 ; i < maxButtons ; ++i) {
1581     BookmarkButton* button = [buttons_ objectAtIndex:i];
1582     if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix])
1583       return i;
1584   }
1585   return -1;
1586 }
1587
1588 - (void)setSelectedButtonByPrefix:(NSString*)prefix {
1589   [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]];
1590 }
1591
1592 - (void)selectPrevious {
1593   int newIndex;
1594   if (selectedIndex_ == 0)
1595     return;
1596   if (selectedIndex_ < 0)
1597     newIndex = [self buttonCount] -1;
1598   else
1599     newIndex = std::max(selectedIndex_ - 1, 0);
1600   [self setSelectedButtonByIndex:newIndex];
1601 }
1602
1603 - (void)selectNext {
1604   if (selectedIndex_ + 1 < [self buttonCount])
1605     [self setSelectedButtonByIndex:selectedIndex_ + 1];
1606 }
1607
1608 - (BOOL)handleInputText:(NSString*)newText {
1609   const unichar kUnicodeEscape = 0x001B;
1610   const unichar kUnicodeSpace = 0x0020;
1611
1612   // Event goes to the deepest nested open submenu.
1613   if (folderController_)
1614     return [folderController_ handleInputText:newText];
1615
1616   // Look for arrow keys or other function keys.
1617   if ([newText length] == 1) {
1618     // Get the 16-bit unicode char.
1619     unichar theChar = [newText characterAtIndex:0];
1620     switch (theChar) {
1621
1622       // Keys that trigger opening of the selection.
1623       case kUnicodeSpace: // Space.
1624       case NSNewlineCharacter:
1625       case NSCarriageReturnCharacter:
1626       case NSEnterCharacter:
1627         if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) {
1628           [barController_ openBookmark:[buttons_ objectAtIndex:selectedIndex_]];
1629           return NO; // NO because the selection-handling code will close later.
1630         } else {
1631           return YES; // Triggering with no selection closes the menu.
1632         }
1633       // Keys that cancel and close the menu.
1634       case kUnicodeEscape:
1635       case NSDeleteCharacter:
1636       case NSBackspaceCharacter:
1637         [self clearInputText];
1638         return YES;
1639       // Keys that change selection directionally.
1640       case NSUpArrowFunctionKey:
1641         [self clearInputText];
1642         [self selectPrevious];
1643         return NO;
1644       case NSDownArrowFunctionKey:
1645         [self clearInputText];
1646         [self selectNext];
1647         return NO;
1648       // Keys that open and close submenus.
1649       case NSRightArrowFunctionKey: {
1650         BookmarkButton* btn = [self buttonAtIndex:selectedIndex_];
1651         if (btn && [btn isFolder]) {
1652           [self openBookmarkFolderFromButtonAndCloseOldOne:btn];
1653           [folderController_ selectNext];
1654         }
1655         [self clearInputText];
1656         return NO;
1657       }
1658       case NSLeftArrowFunctionKey:
1659         [self clearInputText];
1660         [parentController_ closeBookmarkFolder:self];
1661         return NO;
1662
1663       // Check for other keys that should close the menu.
1664       default: {
1665         if (theChar > NSUpArrowFunctionKey &&
1666             theChar <= NSModeSwitchFunctionKey) {
1667           [self clearInputText];
1668           return YES;
1669         }
1670         break;
1671       }
1672     }
1673   }
1674
1675   // It is a char or string worth adding to the type-select buffer.
1676   NSString* newString = (!typedPrefix_) ?
1677       newText : [typedPrefix_ stringByAppendingString:newText];
1678   [typedPrefix_ release];
1679   typedPrefix_ = [newString retain];
1680   [self setSelectedButtonByPrefix:typedPrefix_];
1681   return NO;
1682 }
1683
1684 // Return the y position for a drop indicator.
1685 //
1686 // TODO(jrg): again we have code dup, sort of, with
1687 // bookmark_bar_controller.mm, but the axis is changed.
1688 // http://crbug.com/35966
1689 - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
1690   CGFloat y = 0;
1691   int destIndex = [self indexForDragToPoint:point];
1692   int numButtons = static_cast<int>([buttons_ count]);
1693
1694   // If it's a drop strictly between existing buttons or at the very beginning
1695   if (destIndex >= 0 && destIndex < numButtons) {
1696     // ... put the indicator right between the buttons.
1697     BookmarkButton* button =
1698         [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)];
1699     DCHECK(button);
1700     NSRect buttonFrame = [button frame];
1701     y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding;
1702
1703     // If it's a drop at the end (past the last button, if there are any) ...
1704   } else if (destIndex == numButtons) {
1705     // and if it's past the last button ...
1706     if (numButtons > 0) {
1707       // ... find the last button, and put the indicator below it.
1708       BookmarkButton* button =
1709           [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
1710       DCHECK(button);
1711       NSRect buttonFrame = [button frame];
1712       y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding;
1713
1714     }
1715   } else {
1716     NOTREACHED();
1717   }
1718
1719   return y;
1720 }
1721
1722 - (ThemeService*)themeService {
1723   return [parentController_ themeService];
1724 }
1725
1726 - (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
1727   // Do nothing.
1728 }
1729
1730 - (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
1731   // Do nothing.
1732 }
1733
1734 - (BookmarkBarFolderController*)folderController {
1735   return folderController_;
1736 }
1737
1738 - (void)faviconLoadedForNode:(const BookmarkNode*)node {
1739   for (BookmarkButton* button in buttons_.get()) {
1740     if ([button bookmarkNode] == node) {
1741       [button setImage:[barController_ faviconForNode:node]];
1742       [button setNeedsDisplay:YES];
1743       return;
1744     }
1745   }
1746
1747   // Node was not in this menu, try submenu.
1748   if (folderController_)
1749     [folderController_ faviconLoadedForNode:node];
1750 }
1751
1752 // Add a new folder controller as triggered by the given folder button.
1753 - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
1754   if (folderController_)
1755     [self closeBookmarkFolder:self];
1756
1757   // Folder controller, like many window controllers, owns itself.
1758   folderController_ =
1759       [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
1760                                                parentController:self
1761                                                   barController:barController_
1762                                                         profile:profile_];
1763   [folderController_ showWindow:self];
1764 }
1765
1766 - (void)openAll:(const BookmarkNode*)node
1767     disposition:(WindowOpenDisposition)disposition {
1768   [barController_ openAll:node disposition:disposition];
1769 }
1770
1771 - (void)addButtonForNode:(const BookmarkNode*)node
1772                  atIndex:(NSInteger)buttonIndex {
1773   // Propose the frame for the new button. By default, this will be set to the
1774   // topmost button's frame (and there will always be one) offset upward in
1775   // anticipation of insertion.
1776   NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
1777   newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1778   // When adding a button to an empty folder we must remove the 'empty'
1779   // placeholder button. This can be detected by checking for a parent
1780   // child count of 1.
1781   const BookmarkNode* parentNode = node->parent();
1782   if (parentNode->child_count() == 1) {
1783     BookmarkButton* emptyButton = [buttons_ lastObject];
1784     newButtonFrame = [emptyButton frame];
1785     [emptyButton setDelegate:nil];
1786     [emptyButton removeFromSuperview];
1787     [buttons_ removeLastObject];
1788   }
1789
1790   if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
1791     buttonIndex = [buttons_ count];
1792
1793   // Offset upward by one button height all buttons above insertion location.
1794   BookmarkButton* button = nil;  // Remember so it can be de-highlighted.
1795   for (NSInteger i = 0; i < buttonIndex; ++i) {
1796     button = [buttons_ objectAtIndex:i];
1797     // Remember this location in case it's the last button being moved
1798     // which is where the new button will be located.
1799     newButtonFrame = [button frame];
1800     NSRect buttonFrame = [button frame];
1801     buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1802     [button setFrame:buttonFrame];
1803   }
1804   [[button cell] mouseExited:nil];  // De-highlight.
1805   BookmarkButton* newButton = [self makeButtonForNode:node
1806                                                 frame:newButtonFrame];
1807   [buttons_ insertObject:newButton atIndex:buttonIndex];
1808   [folderView_ addSubview:newButton];
1809
1810   // Close any child folder(s) which may still be open.
1811   [self closeBookmarkFolder:self];
1812
1813   [self adjustWindowForButtonCount:[buttons_ count]];
1814 }
1815
1816 // More code which essentially duplicates that of BookmarkBarController.
1817 // TODO(mrossetti,jrg): http://crbug.com/35966
1818 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
1819   DCHECK([urls count] == [titles count]);
1820   BOOL nodesWereAdded = NO;
1821   // Figure out where these new bookmarks nodes are to be added.
1822   BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1823   BookmarkModel* bookmarkModel = [self bookmarkModel];
1824   const BookmarkNode* destParent = NULL;
1825   int destIndex = 0;
1826   if ([button isFolder]) {
1827     destParent = [button bookmarkNode];
1828     // Drop it at the end.
1829     destIndex = [button bookmarkNode]->child_count();
1830   } else {
1831     // Else we're dropping somewhere in the folder, so find the right spot.
1832     destParent = [parentButton_ bookmarkNode];
1833     destIndex = [self indexForDragToPoint:point];
1834     // Be careful if the number of buttons != number of nodes.
1835     destIndex += [[parentButton_ cell] startingChildIndex];
1836   }
1837
1838   ChromeBookmarkClient* client =
1839       ChromeBookmarkClientFactory::GetForProfile(profile_);
1840   if (!client->CanBeEditedByUser(destParent))
1841     return NO;
1842
1843   // Create and add the new bookmark nodes.
1844   size_t urlCount = [urls count];
1845   for (size_t i = 0; i < urlCount; ++i) {
1846     GURL gurl;
1847     const char* string = [[urls objectAtIndex:i] UTF8String];
1848     if (string)
1849       gurl = GURL(string);
1850     // We only expect to receive valid URLs.
1851     DCHECK(gurl.is_valid());
1852     if (gurl.is_valid()) {
1853       bookmarkModel->AddURL(destParent,
1854                             destIndex++,
1855                             base::SysNSStringToUTF16([titles objectAtIndex:i]),
1856                             gurl);
1857       nodesWereAdded = YES;
1858     }
1859   }
1860   return nodesWereAdded;
1861 }
1862
1863 - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
1864   if (fromIndex != toIndex) {
1865     if (toIndex == -1)
1866       toIndex = [buttons_ count];
1867     BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
1868     if (movedButton == buttonThatMouseIsIn_)
1869       buttonThatMouseIsIn_ = nil;
1870     [buttons_ removeObjectAtIndex:fromIndex];
1871     NSRect movedFrame = [movedButton frame];
1872     NSPoint toOrigin = movedFrame.origin;
1873     [movedButton setHidden:YES];
1874     if (fromIndex < toIndex) {
1875       BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
1876       toOrigin = [targetButton frame].origin;
1877       for (NSInteger i = fromIndex; i < toIndex; ++i) {
1878         BookmarkButton* button = [buttons_ objectAtIndex:i];
1879         NSRect frame = [button frame];
1880         frame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1881         [button setFrameOrigin:frame.origin];
1882       }
1883     } else {
1884       BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
1885       toOrigin = [targetButton frame].origin;
1886       for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
1887         BookmarkButton* button = [buttons_ objectAtIndex:i];
1888         NSRect buttonFrame = [button frame];
1889         buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1890         [button setFrameOrigin:buttonFrame.origin];
1891       }
1892     }
1893     [buttons_ insertObject:movedButton atIndex:toIndex];
1894     [movedButton setFrameOrigin:toOrigin];
1895     [movedButton setHidden:NO];
1896   }
1897 }
1898
1899 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1900 - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
1901   // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
1902   BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
1903   NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
1904
1905   // If this button has an open sub-folder, close it.
1906   if ([folderController_ parentButton] == oldButton)
1907     [self closeBookmarkFolder:self];
1908
1909   // If a hover-open is pending, cancel it.
1910   if (oldButton == buttonThatMouseIsIn_) {
1911     [NSObject cancelPreviousPerformRequestsWithTarget:self];
1912     buttonThatMouseIsIn_ = nil;
1913   }
1914
1915   // Deleting a button causes rearrangement that enables us to lose a
1916   // mouse-exited event.  This problem doesn't appear to exist with
1917   // other keep-menu-open options (e.g. add folder).  Since the
1918   // showsBorderOnlyWhileMouseInside uses a tracking area, simple
1919   // tricks (e.g. sending an extra mouseExited: to the button) don't
1920   // fix the problem.
1921   // http://crbug.com/54324
1922   for (NSButton* button in buttons_.get()) {
1923     if ([button showsBorderOnlyWhileMouseInside]) {
1924       [button setShowsBorderOnlyWhileMouseInside:NO];
1925       [button setShowsBorderOnlyWhileMouseInside:YES];
1926     }
1927   }
1928
1929   [oldButton setDelegate:nil];
1930   [oldButton removeFromSuperview];
1931   [buttons_ removeObjectAtIndex:buttonIndex];
1932   for (NSInteger i = 0; i < buttonIndex; ++i) {
1933     BookmarkButton* button = [buttons_ objectAtIndex:i];
1934     NSRect buttonFrame = [button frame];
1935     buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1936     [button setFrame:buttonFrame];
1937   }
1938   // Search for and adjust submenus, if necessary.
1939   NSInteger buttonCount = [buttons_ count];
1940   if (buttonCount) {
1941     BookmarkButton* subButton = [folderController_ parentButton];
1942     NSInteger targetIndex = 0;
1943     for (NSButton* aButton in buttons_.get()) {
1944       targetIndex++;
1945       // If this button is showing its menu and is below the removed button,
1946       // i.e its index is greater, then we need to move the menu too.
1947       if (aButton == subButton && targetIndex > buttonIndex) {
1948           [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0,
1949                                  bookmarks::kBookmarkFolderButtonHeight)];
1950           break;
1951       }
1952     }
1953   } else if (parentButton_ != [barController_ otherBookmarksButton]) {
1954     // If all nodes have been removed from this folder then add in the
1955     // 'empty' placeholder button except for "Other bookmarks" folder
1956     // as we are going to hide it.
1957     NSRect buttonFrame =
1958         GetFirstButtonFrameForHeight([self menuHeightForButtonCount:1]);
1959     BookmarkButton* button = [self makeButtonForNode:nil
1960                                                frame:buttonFrame];
1961     [buttons_ addObject:button];
1962     [folderView_ addSubview:button];
1963     buttonCount = 1;
1964   }
1965
1966   // buttonCount will be 0 if "Other bookmarks" folder is empty, so close
1967   // the folder before hiding it.
1968   if (buttonCount == 0)
1969     [barController_ closeBookmarkFolder:nil];
1970   else if (buttonCount > 0)
1971     [self adjustWindowForButtonCount:buttonCount];
1972
1973   if (animate && !ignoreAnimations_)
1974     NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
1975                           NSZeroSize, nil, nil, nil);
1976 }
1977
1978 - (id<BookmarkButtonControllerProtocol>)controllerForNode:
1979     (const BookmarkNode*)node {
1980   // See if we are holding this node, otherwise see if it is in our
1981   // hierarchy of visible folder menus.
1982   if ([parentButton_ bookmarkNode] == node)
1983     return self;
1984   return [folderController_ controllerForNode:node];
1985 }
1986
1987 #pragma mark TestingAPI Only
1988
1989 - (BOOL)canScrollUp {
1990   return ![scrollUpArrowView_ isHidden];
1991 }
1992
1993 - (BOOL)canScrollDown {
1994   return ![scrollDownArrowView_ isHidden];
1995 }
1996
1997 - (CGFloat)verticalScrollArrowHeight {
1998   return verticalScrollArrowHeight_;
1999 }
2000
2001 - (NSView*)visibleView {
2002   return visibleView_;
2003 }
2004
2005 - (NSScrollView*)scrollView {
2006   return scrollView_;
2007 }
2008
2009 - (NSView*)folderView {
2010   return folderView_;
2011 }
2012
2013 - (void)setIgnoreAnimations:(BOOL)ignore {
2014   ignoreAnimations_ = ignore;
2015 }
2016
2017 - (BookmarkButton*)buttonThatMouseIsIn {
2018   return buttonThatMouseIsIn_;
2019 }
2020
2021 @end  // BookmarkBarFolderController