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