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