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