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