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