Upstream version 10.39.225.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.  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:buttonCount];
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
1149 static 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   // If right click was done immediately on entering a button, then open the
1338   // folder without delay so that context menu appears over the folder menu.
1339   if ([event type] == NSRightMouseDown)
1340     [self openBookmarkFolderFromButtonAndCloseOldOne:sender];
1341   else
1342     [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:)
1343                withObject:sender
1344                afterDelay:bookmarks::kHoverOpenDelay
1345                   inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
1346 }
1347
1348 // Called from the BookmarkButton
1349 - (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
1350   if (buttonThatMouseIsIn_ == sender)
1351     buttonThatMouseIsIn_ = nil;
1352     [self setSelectedButtonByIndex:-1];
1353
1354   // Stop any timer about opening a new hover-open folder.
1355
1356   // Since a performSelector:withDelay: on self retains self, it is
1357   // possible that a cancelPreviousPerformRequestsWithTarget: reduces
1358   // the refcount to 0, releasing us.  That's a bad thing to do while
1359   // this object (or others it may own) is in the event chain.  Thus
1360   // we have a retain/autorelease.
1361   [self retain];
1362   [NSObject cancelPreviousPerformRequestsWithTarget:self];
1363   [self autorelease];
1364 }
1365
1366 - (NSWindow*)browserWindow {
1367   return [barController_ browserWindow];
1368 }
1369
1370 - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
1371   return [barController_ canEditBookmarks] &&
1372          [barController_ canEditBookmark:[button bookmarkNode]];
1373 }
1374
1375 - (void)didDragBookmarkToTrash:(BookmarkButton*)button {
1376   [barController_ didDragBookmarkToTrash:button];
1377 }
1378
1379 - (void)bookmarkDragDidEnd:(BookmarkButton*)button
1380                  operation:(NSDragOperation)operation {
1381   [barController_ bookmarkDragDidEnd:button
1382                            operation:operation];
1383 }
1384
1385
1386 #pragma mark BookmarkButtonControllerProtocol
1387
1388 // Recursively close all bookmark folders.
1389 - (void)closeAllBookmarkFolders {
1390   // Closing the top level implicitly closes all children.
1391   [barController_ closeAllBookmarkFolders];
1392 }
1393
1394 // Close our bookmark folder (a sub-controller) if we have one.
1395 - (void)closeBookmarkFolder:(id)sender {
1396   if (folderController_) {
1397     // Make this menu key, so key status doesn't go back to the browser
1398     // window when the submenu closes.
1399     [[self window] makeKeyWindow];
1400     [self setSubFolderGrowthToRight:YES];
1401     [[folderController_ window] close];
1402     folderController_ = nil;
1403   }
1404 }
1405
1406 - (BookmarkModel*)bookmarkModel {
1407   return [barController_ bookmarkModel];
1408 }
1409
1410 - (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
1411   return [barController_ draggingAllowed:info];
1412 }
1413
1414 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1415 // Most of the work (e.g. drop indicator) is taken care of in the
1416 // folder_view.  Here we handle hover open issues for subfolders.
1417 // Caution: there are subtle differences between this one and
1418 // bookmark_bar_controller.mm's version.
1419 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
1420   NSPoint currentLocation = [info draggingLocation];
1421   BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
1422
1423   // Don't allow drops that would result in cycles.
1424   if (button) {
1425     NSData* data = [[info draggingPasteboard]
1426                     dataForType:kBookmarkButtonDragType];
1427     if (data && [info draggingSource]) {
1428       BookmarkButton* sourceButton = nil;
1429       [data getBytes:&sourceButton length:sizeof(sourceButton)];
1430       const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1431       const BookmarkNode* destNode = [button bookmarkNode];
1432       if (destNode->HasAncestor(sourceNode))
1433         button = nil;
1434     }
1435   }
1436   // Delegate handling of dragging over a button to the |hoverState_| member.
1437   return [hoverState_ draggingEnteredButton:button];
1438 }
1439
1440 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info {
1441   return NSDragOperationMove;
1442 }
1443
1444 // Unlike bookmark_bar_controller, we need to keep track of dragging state.
1445 // We also need to make sure we cancel the delayed hover close.
1446 - (void)draggingExited:(id<NSDraggingInfo>)info {
1447   // NOT the same as a cancel --> we may have moved the mouse into the submenu.
1448   // Delegate handling of the hover button to the |hoverState_| member.
1449   [hoverState_ draggingExited];
1450 }
1451
1452 - (BOOL)dragShouldLockBarVisibility {
1453   return [parentController_ dragShouldLockBarVisibility];
1454 }
1455
1456 // TODO(jrg): ARGH more code dup.
1457 // http://crbug.com/35966
1458 - (BOOL)dragButton:(BookmarkButton*)sourceButton
1459                 to:(NSPoint)point
1460               copy:(BOOL)copy {
1461   DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
1462   const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1463   return [self dragBookmark:sourceNode to:point copy:copy];
1464 }
1465
1466 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1467 // http://crbug.com/35966
1468 - (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
1469   BOOL dragged = NO;
1470   std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
1471   if (nodes.size()) {
1472     BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
1473     NSPoint dropPoint = [info draggingLocation];
1474     for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
1475          it != nodes.end(); ++it) {
1476       const BookmarkNode* sourceNode = *it;
1477       dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
1478     }
1479   }
1480   return dragged;
1481 }
1482
1483 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1484 // http://crbug.com/35966
1485 - (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
1486   std::vector<const BookmarkNode*> dragDataNodes;
1487   BookmarkNodeData dragData;
1488   if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) {
1489     BookmarkModel* bookmarkModel = [self bookmarkModel];
1490     std::vector<const BookmarkNode*> nodes(
1491         dragData.GetNodes(bookmarkModel, profile_->GetPath()));
1492     dragDataNodes.assign(nodes.begin(), nodes.end());
1493   }
1494   return dragDataNodes;
1495 }
1496
1497 // Return YES if we should show the drop indicator, else NO.
1498 // TODO(jrg): ARGH code dup!
1499 // http://crbug.com/35966
1500 - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
1501   return ![self buttonForDroppingOnAtPoint:point];
1502 }
1503
1504 // Button selection change code to support type to select and arrow key events.
1505 #pragma mark Keyboard Support
1506
1507 // Scroll the menu to show the selected button, if it's not already visible.
1508 - (void)showSelectedButton {
1509   int bMaxIndex = [self buttonCount] - 1; // Max array index in button array.
1510
1511   // Is there a valid selected button?
1512   if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex)
1513     return;
1514
1515   // Is the menu scrollable anyway?
1516   if (![self canScrollUp] && ![self canScrollDown])
1517     return;
1518
1519   // Now check to see if we need to scroll, which way, and how far.
1520   CGFloat delta = 0.0;
1521   NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
1522   CGFloat itemBottom = (bMaxIndex - selectedIndex_) *
1523       bookmarks::kBookmarkFolderButtonHeight;
1524   CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight;
1525   CGFloat viewHeight = NSHeight([scrollView_  frame]);
1526
1527   if (scrollPoint.y > itemBottom) { // Need to scroll down.
1528     delta = scrollPoint.y - itemBottom;
1529   } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up.
1530     delta = -(itemTop - (scrollPoint.y + viewHeight));
1531   } else { // No need to scroll.
1532     return;
1533   }
1534
1535   [self performOneScroll:delta];
1536 }
1537
1538 // All changes to selectedness of buttons (aka fake menu items) ends up
1539 // calling this method to actually flip the state of items.
1540 // Needs to handle -1 as the invalid index (when nothing is selected) and
1541 // greater than range values too.
1542 - (void)setStateOfButtonByIndex:(int)index
1543                           state:(bool)state {
1544   if (index >= 0 && index < [self buttonCount])
1545     [[buttons_ objectAtIndex:index] highlight:state];
1546 }
1547
1548 // Selects the required button and deselects the previously selected one.
1549 // An index of -1 means no selection.
1550 - (void)setSelectedButtonByIndex:(int)index {
1551   if (index == selectedIndex_)
1552     return;
1553
1554   [self setStateOfButtonByIndex:selectedIndex_ state:NO];
1555   [self setStateOfButtonByIndex:index state:YES];
1556   selectedIndex_ = index;
1557
1558   [self showSelectedButton];
1559 }
1560
1561 - (void)clearInputText {
1562   [typedPrefix_ release];
1563   typedPrefix_ = nil;
1564 }
1565
1566 // Find the earliest item in the folder which has the target prefix.
1567 // Returns nil if there is no prefix or there are no matches.
1568 // These are in no particular order, and not particularly numerous, so linear
1569 // search should be OK.
1570 // -1 means no match.
1571 - (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix {
1572   if ([prefix length] == 0) // Also handles nil.
1573     return -1;
1574   int maxButtons = [buttons_ count];
1575   NSString* lowercasePrefix = [prefix lowercaseString];
1576   for (int i = 0 ; i < maxButtons ; ++i) {
1577     BookmarkButton* button = [buttons_ objectAtIndex:i];
1578     if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix])
1579       return i;
1580   }
1581   return -1;
1582 }
1583
1584 - (void)setSelectedButtonByPrefix:(NSString*)prefix {
1585   [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]];
1586 }
1587
1588 - (void)selectPrevious {
1589   int newIndex;
1590   if (selectedIndex_ == 0)
1591     return;
1592   if (selectedIndex_ < 0)
1593     newIndex = [self buttonCount] -1;
1594   else
1595     newIndex = std::max(selectedIndex_ - 1, 0);
1596   [self setSelectedButtonByIndex:newIndex];
1597 }
1598
1599 - (void)selectNext {
1600   if (selectedIndex_ + 1 < [self buttonCount])
1601     [self setSelectedButtonByIndex:selectedIndex_ + 1];
1602 }
1603
1604 - (BOOL)handleInputText:(NSString*)newText {
1605   const unichar kUnicodeEscape = 0x001B;
1606   const unichar kUnicodeSpace = 0x0020;
1607
1608   // Event goes to the deepest nested open submenu.
1609   if (folderController_)
1610     return [folderController_ handleInputText:newText];
1611
1612   // Look for arrow keys or other function keys.
1613   if ([newText length] == 1) {
1614     // Get the 16-bit unicode char.
1615     unichar theChar = [newText characterAtIndex:0];
1616     switch (theChar) {
1617
1618       // Keys that trigger opening of the selection.
1619       case kUnicodeSpace: // Space.
1620       case NSNewlineCharacter:
1621       case NSCarriageReturnCharacter:
1622       case NSEnterCharacter:
1623         if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) {
1624           [barController_ openBookmark:[buttons_ objectAtIndex:selectedIndex_]];
1625           return NO; // NO because the selection-handling code will close later.
1626         } else {
1627           return YES; // Triggering with no selection closes the menu.
1628         }
1629       // Keys that cancel and close the menu.
1630       case kUnicodeEscape:
1631       case NSDeleteCharacter:
1632       case NSBackspaceCharacter:
1633         [self clearInputText];
1634         return YES;
1635       // Keys that change selection directionally.
1636       case NSUpArrowFunctionKey:
1637         [self clearInputText];
1638         [self selectPrevious];
1639         return NO;
1640       case NSDownArrowFunctionKey:
1641         [self clearInputText];
1642         [self selectNext];
1643         return NO;
1644       // Keys that open and close submenus.
1645       case NSRightArrowFunctionKey: {
1646         BookmarkButton* btn = [self buttonAtIndex:selectedIndex_];
1647         if (btn && [btn isFolder]) {
1648           [self openBookmarkFolderFromButtonAndCloseOldOne:btn];
1649           [folderController_ selectNext];
1650         }
1651         [self clearInputText];
1652         return NO;
1653       }
1654       case NSLeftArrowFunctionKey:
1655         [self clearInputText];
1656         [parentController_ closeBookmarkFolder:self];
1657         return NO;
1658
1659       // Check for other keys that should close the menu.
1660       default: {
1661         if (theChar > NSUpArrowFunctionKey &&
1662             theChar <= NSModeSwitchFunctionKey) {
1663           [self clearInputText];
1664           return YES;
1665         }
1666         break;
1667       }
1668     }
1669   }
1670
1671   // It is a char or string worth adding to the type-select buffer.
1672   NSString* newString = (!typedPrefix_) ?
1673       newText : [typedPrefix_ stringByAppendingString:newText];
1674   [typedPrefix_ release];
1675   typedPrefix_ = [newString retain];
1676   [self setSelectedButtonByPrefix:typedPrefix_];
1677   return NO;
1678 }
1679
1680 // Return the y position for a drop indicator.
1681 //
1682 // TODO(jrg): again we have code dup, sort of, with
1683 // bookmark_bar_controller.mm, but the axis is changed.
1684 // http://crbug.com/35966
1685 - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
1686   CGFloat y = 0;
1687   int destIndex = [self indexForDragToPoint:point];
1688   int numButtons = static_cast<int>([buttons_ count]);
1689
1690   // If it's a drop strictly between existing buttons or at the very beginning
1691   if (destIndex >= 0 && destIndex < numButtons) {
1692     // ... put the indicator right between the buttons.
1693     BookmarkButton* button =
1694         [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)];
1695     DCHECK(button);
1696     NSRect buttonFrame = [button frame];
1697     y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding;
1698
1699     // If it's a drop at the end (past the last button, if there are any) ...
1700   } else if (destIndex == numButtons) {
1701     // and if it's past the last button ...
1702     if (numButtons > 0) {
1703       // ... find the last button, and put the indicator below it.
1704       BookmarkButton* button =
1705           [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
1706       DCHECK(button);
1707       NSRect buttonFrame = [button frame];
1708       y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding;
1709
1710     }
1711   } else {
1712     NOTREACHED();
1713   }
1714
1715   return y;
1716 }
1717
1718 - (ThemeService*)themeService {
1719   return [parentController_ themeService];
1720 }
1721
1722 - (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
1723   // Do nothing.
1724 }
1725
1726 - (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
1727   // Do nothing.
1728 }
1729
1730 - (BookmarkBarFolderController*)folderController {
1731   return folderController_;
1732 }
1733
1734 - (void)faviconLoadedForNode:(const BookmarkNode*)node {
1735   for (BookmarkButton* button in buttons_.get()) {
1736     if ([button bookmarkNode] == node) {
1737       [button setImage:[barController_ faviconForNode:node]];
1738       [button setNeedsDisplay:YES];
1739       return;
1740     }
1741   }
1742
1743   // Node was not in this menu, try submenu.
1744   if (folderController_)
1745     [folderController_ faviconLoadedForNode:node];
1746 }
1747
1748 // Add a new folder controller as triggered by the given folder button.
1749 - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
1750   if (folderController_)
1751     [self closeBookmarkFolder:self];
1752
1753   // Folder controller, like many window controllers, owns itself.
1754   folderController_ =
1755       [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
1756                                                parentController:self
1757                                                   barController:barController_
1758                                                         profile:profile_];
1759   [folderController_ showWindow:self];
1760 }
1761
1762 - (void)openAll:(const BookmarkNode*)node
1763     disposition:(WindowOpenDisposition)disposition {
1764   [barController_ openAll:node disposition:disposition];
1765 }
1766
1767 - (void)addButtonForNode:(const BookmarkNode*)node
1768                  atIndex:(NSInteger)buttonIndex {
1769   // Propose the frame for the new button. By default, this will be set to the
1770   // topmost button's frame (and there will always be one) offset upward in
1771   // anticipation of insertion.
1772   NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
1773   newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1774   // When adding a button to an empty folder we must remove the 'empty'
1775   // placeholder button. This can be detected by checking for a parent
1776   // child count of 1.
1777   const BookmarkNode* parentNode = node->parent();
1778   if (parentNode->child_count() == 1) {
1779     BookmarkButton* emptyButton = [buttons_ lastObject];
1780     newButtonFrame = [emptyButton frame];
1781     [emptyButton setDelegate:nil];
1782     [emptyButton removeFromSuperview];
1783     [buttons_ removeLastObject];
1784   }
1785
1786   if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
1787     buttonIndex = [buttons_ count];
1788
1789   // Offset upward by one button height all buttons above insertion location.
1790   BookmarkButton* button = nil;  // Remember so it can be de-highlighted.
1791   for (NSInteger i = 0; i < buttonIndex; ++i) {
1792     button = [buttons_ objectAtIndex:i];
1793     // Remember this location in case it's the last button being moved
1794     // which is where the new button will be located.
1795     newButtonFrame = [button frame];
1796     NSRect buttonFrame = [button frame];
1797     buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1798     [button setFrame:buttonFrame];
1799   }
1800   [[button cell] mouseExited:nil];  // De-highlight.
1801   BookmarkButton* newButton = [self makeButtonForNode:node
1802                                                 frame:newButtonFrame];
1803   [buttons_ insertObject:newButton atIndex:buttonIndex];
1804   [folderView_ addSubview:newButton];
1805
1806   // Close any child folder(s) which may still be open.
1807   [self closeBookmarkFolder:self];
1808
1809   [self adjustWindowForButtonCount:[buttons_ count]];
1810 }
1811
1812 // More code which essentially duplicates that of BookmarkBarController.
1813 // TODO(mrossetti,jrg): http://crbug.com/35966
1814 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
1815   DCHECK([urls count] == [titles count]);
1816   BOOL nodesWereAdded = NO;
1817   // Figure out where these new bookmarks nodes are to be added.
1818   BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1819   BookmarkModel* bookmarkModel = [self bookmarkModel];
1820   const BookmarkNode* destParent = NULL;
1821   int destIndex = 0;
1822   if ([button isFolder]) {
1823     destParent = [button bookmarkNode];
1824     // Drop it at the end.
1825     destIndex = [button bookmarkNode]->child_count();
1826   } else {
1827     // Else we're dropping somewhere in the folder, so find the right spot.
1828     destParent = [parentButton_ bookmarkNode];
1829     destIndex = [self indexForDragToPoint:point];
1830     // Be careful if the number of buttons != number of nodes.
1831     destIndex += [[parentButton_ cell] startingChildIndex];
1832   }
1833
1834   ChromeBookmarkClient* client =
1835       ChromeBookmarkClientFactory::GetForProfile(profile_);
1836   if (!client->CanBeEditedByUser(destParent))
1837     return NO;
1838
1839   // Create and add the new bookmark nodes.
1840   size_t urlCount = [urls count];
1841   for (size_t i = 0; i < urlCount; ++i) {
1842     GURL gurl;
1843     const char* string = [[urls objectAtIndex:i] UTF8String];
1844     if (string)
1845       gurl = GURL(string);
1846     // We only expect to receive valid URLs.
1847     DCHECK(gurl.is_valid());
1848     if (gurl.is_valid()) {
1849       bookmarkModel->AddURL(destParent,
1850                             destIndex++,
1851                             base::SysNSStringToUTF16([titles objectAtIndex:i]),
1852                             gurl);
1853       nodesWereAdded = YES;
1854     }
1855   }
1856   return nodesWereAdded;
1857 }
1858
1859 - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
1860   if (fromIndex != toIndex) {
1861     if (toIndex == -1)
1862       toIndex = [buttons_ count];
1863     BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
1864     if (movedButton == buttonThatMouseIsIn_)
1865       buttonThatMouseIsIn_ = nil;
1866     [buttons_ removeObjectAtIndex:fromIndex];
1867     NSRect movedFrame = [movedButton frame];
1868     NSPoint toOrigin = movedFrame.origin;
1869     [movedButton setHidden:YES];
1870     if (fromIndex < toIndex) {
1871       BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
1872       toOrigin = [targetButton frame].origin;
1873       for (NSInteger i = fromIndex; i < toIndex; ++i) {
1874         BookmarkButton* button = [buttons_ objectAtIndex:i];
1875         NSRect frame = [button frame];
1876         frame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1877         [button setFrameOrigin:frame.origin];
1878       }
1879     } else {
1880       BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
1881       toOrigin = [targetButton frame].origin;
1882       for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
1883         BookmarkButton* button = [buttons_ objectAtIndex:i];
1884         NSRect buttonFrame = [button frame];
1885         buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1886         [button setFrameOrigin:buttonFrame.origin];
1887       }
1888     }
1889     [buttons_ insertObject:movedButton atIndex:toIndex];
1890     [movedButton setFrameOrigin:toOrigin];
1891     [movedButton setHidden:NO];
1892   }
1893 }
1894
1895 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1896 - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
1897   // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
1898   BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
1899   NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
1900
1901   // If this button has an open sub-folder, close it.
1902   if ([folderController_ parentButton] == oldButton)
1903     [self closeBookmarkFolder:self];
1904
1905   // If a hover-open is pending, cancel it.
1906   if (oldButton == buttonThatMouseIsIn_) {
1907     [NSObject cancelPreviousPerformRequestsWithTarget:self];
1908     buttonThatMouseIsIn_ = nil;
1909   }
1910
1911   // Deleting a button causes rearrangement that enables us to lose a
1912   // mouse-exited event.  This problem doesn't appear to exist with
1913   // other keep-menu-open options (e.g. add folder).  Since the
1914   // showsBorderOnlyWhileMouseInside uses a tracking area, simple
1915   // tricks (e.g. sending an extra mouseExited: to the button) don't
1916   // fix the problem.
1917   // http://crbug.com/54324
1918   for (NSButton* button in buttons_.get()) {
1919     if ([button showsBorderOnlyWhileMouseInside]) {
1920       [button setShowsBorderOnlyWhileMouseInside:NO];
1921       [button setShowsBorderOnlyWhileMouseInside:YES];
1922     }
1923   }
1924
1925   [oldButton setDelegate:nil];
1926   [oldButton removeFromSuperview];
1927   [buttons_ removeObjectAtIndex:buttonIndex];
1928   for (NSInteger i = 0; i < buttonIndex; ++i) {
1929     BookmarkButton* button = [buttons_ objectAtIndex:i];
1930     NSRect buttonFrame = [button frame];
1931     buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1932     [button setFrame:buttonFrame];
1933   }
1934   // Search for and adjust submenus, if necessary.
1935   NSInteger buttonCount = [buttons_ count];
1936   if (buttonCount) {
1937     BookmarkButton* subButton = [folderController_ parentButton];
1938     for (NSButton* aButton in buttons_.get()) {
1939       // If this button is showing its menu then we need to move the menu, too.
1940       if (aButton == subButton)
1941         [folderController_
1942             offsetFolderMenuWindow:NSMakeSize(0.0, chrome::kBookmarkBarHeight)];
1943     }
1944   } else if (parentButton_ != [barController_ otherBookmarksButton]) {
1945     // If all nodes have been removed from this folder then add in the
1946     // 'empty' placeholder button except for "Other bookmarks" folder
1947     // as we are going to hide it.
1948     NSRect buttonFrame =
1949         GetFirstButtonFrameForHeight([self menuHeightForButtonCount:1]);
1950     BookmarkButton* button = [self makeButtonForNode:nil
1951                                                frame:buttonFrame];
1952     [buttons_ addObject:button];
1953     [folderView_ addSubview:button];
1954     buttonCount = 1;
1955   }
1956
1957   // buttonCount will be 0 if "Other bookmarks" folder is empty, so close
1958   // the folder before hiding it.
1959   if (buttonCount == 0)
1960     [barController_ closeBookmarkFolder:nil];
1961   else if (buttonCount > 0)
1962     [self adjustWindowForButtonCount:buttonCount];
1963
1964   if (animate && !ignoreAnimations_)
1965     NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
1966                           NSZeroSize, nil, nil, nil);
1967 }
1968
1969 - (id<BookmarkButtonControllerProtocol>)controllerForNode:
1970     (const BookmarkNode*)node {
1971   // See if we are holding this node, otherwise see if it is in our
1972   // hierarchy of visible folder menus.
1973   if ([parentButton_ bookmarkNode] == node)
1974     return self;
1975   return [folderController_ controllerForNode:node];
1976 }
1977
1978 #pragma mark TestingAPI Only
1979
1980 - (BOOL)canScrollUp {
1981   return ![scrollUpArrowView_ isHidden];
1982 }
1983
1984 - (BOOL)canScrollDown {
1985   return ![scrollDownArrowView_ isHidden];
1986 }
1987
1988 - (CGFloat)verticalScrollArrowHeight {
1989   return verticalScrollArrowHeight_;
1990 }
1991
1992 - (NSView*)visibleView {
1993   return visibleView_;
1994 }
1995
1996 - (NSScrollView*)scrollView {
1997   return scrollView_;
1998 }
1999
2000 - (NSView*)folderView {
2001   return folderView_;
2002 }
2003
2004 - (void)setIgnoreAnimations:(BOOL)ignore {
2005   ignoreAnimations_ = ignore;
2006 }
2007
2008 - (BookmarkButton*)buttonThatMouseIsIn {
2009   return buttonThatMouseIsIn_;
2010 }
2011
2012 @end  // BookmarkBarFolderController