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