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