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