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/bookmarks/bookmark_model_factory.h"
11 #import "chrome/browser/bookmarks/chrome_bookmark_client.h"
12 #import "chrome/browser/bookmarks/chrome_bookmark_client_factory.h"
13 #import "chrome/browser/profiles/profile.h"
14 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
15 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
16 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
17 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
18 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
19 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
20 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
21 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
22 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
23 #include "components/bookmarks/browser/bookmark_model.h"
24 #include "components/bookmarks/browser/bookmark_node_data.h"
25 #include "ui/base/theme_provider.h"
27 using bookmarks::BookmarkNodeData;
28 using bookmarks::kBookmarkBarMenuCornerRadius;
32 // Frequency of the scrolling timer in seconds.
33 const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1;
35 // Amount to scroll by per timer fire. We scroll rather slowly; to
36 // accomodate we do several at a time.
37 const CGFloat kBookmarkBarFolderScrollAmount =
38 3 * bookmarks::kBookmarkFolderButtonHeight;
40 // Amount to scroll for each scroll wheel roll.
41 const CGFloat kBookmarkBarFolderScrollWheelAmount =
42 1 * bookmarks::kBookmarkFolderButtonHeight;
44 // Determining adjustments to the layout of the folder menu window in response
45 // to resizing and scrolling relies on many visual factors. The following
46 // struct is used to pass around these factors to the several support
47 // functions involved in the adjustment calculations and application.
48 struct LayoutMetrics {
49 // Metrics applied during the final layout adjustments to the window,
50 // the main visible content view, and the menu content view (i.e. the
54 // The proposed and then final scrolling adjustment made to the scrollable
55 // area of the folder menu. This may be modified during the window layout
56 // primarily as a result of hiding or showing the scroll arrows.
62 // The difference between 'could' and 'can' in these next four data members
63 // is this: 'could' represents the previous condition for scrollability
64 // while 'can' represents what the new condition will be for scrollability.
69 // Determines the optimal time during folder menu layout when the contents
70 // of the button scroll area should be scrolled in order to prevent
74 // Intermediate metrics used in determining window vertical layout changes.
75 CGFloat deltaWindowHeight;
77 CGFloat deltaVisibleHeight;
78 CGFloat deltaVisibleY;
79 CGFloat deltaScrollerHeight;
80 CGFloat deltaScrollerY;
82 // Convenience metrics used in multiple functions (carried along here in
83 // order to eliminate the need to calculate in multiple places and
84 // reduce the possibility of bugs).
86 // Bottom of the screen's available area (excluding dock height and padding).
88 // Bottom of the screen.
89 CGFloat screenBottomY;
94 LayoutMetrics(CGFloat windowLeft, NSSize windowSize, CGFloat scrollDelta) :
95 windowLeft(windowLeft),
96 windowSize(windowSize),
97 scrollDelta(scrollDelta),
103 deltaWindowHeight(0.0),
105 deltaVisibleHeight(0.0),
107 deltaScrollerHeight(0.0),
116 NSRect GetFirstButtonFrameForHeight(CGFloat height) {
117 CGFloat y = height - bookmarks::kBookmarkFolderButtonHeight -
118 bookmarks::kBookmarkVerticalPadding;
119 return NSMakeRect(0, y, bookmarks::kDefaultBookmarkWidth,
120 bookmarks::kBookmarkFolderButtonHeight);
126 // Required to set the right tracking bounds for our fake menus.
127 @interface NSView(Private)
128 - (void)_updateTrackingAreas;
131 @interface BookmarkBarFolderController(Private)
132 - (void)configureWindow;
133 - (void)addOrUpdateScrollTracking;
134 - (void)removeScrollTracking;
136 - (void)addScrollTimerWithDelta:(CGFloat)delta;
138 // Helper function to configureWindow which performs a basic layout of
139 // the window subviews, in particular the menu buttons and the window width.
140 - (void)layOutWindowWithHeight:(CGFloat)height;
142 // Determine the best button width (which will be the widest button or the
143 // maximum allowable button width, whichever is less) and resize all buttons.
144 // Return the new width so that the window can be adjusted.
145 - (CGFloat)adjustButtonWidths;
147 // Returns the total menu height needed to display |buttonCount| buttons.
148 // Does not do any fancy tricks like trimming the height to fit on the screen.
149 - (int)menuHeightForButtonCount:(int)buttonCount;
151 // Adjust layout of the folder menu window components, showing/hiding the
152 // scroll up/down arrows, and resizing as necessary for a proper disaplay.
153 // In order to reduce window flicker, all layout changes are deferred until
154 // the final step of the adjustment. To accommodate this deferral, window
155 // height and width changes needed by callers to this function pass their
156 // desired window changes in |size|. When scrolling is to be performed
157 // any scrolling change is given by |scrollDelta|. The ultimate amount of
158 // scrolling may be different from |scrollDelta| in order to accommodate
159 // changes in the scroller view layout. These proposed window adjustments
160 // are passed to helper functions using a LayoutMetrics structure.
162 // This function should be called when: 1) initially setting up a folder menu
163 // window, 2) responding to scrolling of the contents (which may affect the
164 // height of the window), 3) addition or removal of bookmark items (such as
165 // during cut/paste/delete/drag/drop operations).
166 - (void)adjustWindowLeft:(CGFloat)windowLeft
167 size:(NSSize)windowSize
168 scrollingBy:(CGFloat)scrollDelta;
170 // Support function for adjustWindowLeft:size:scrollingBy: which initializes
171 // the layout adjustments by gathering current folder menu window and subviews
172 // positions and sizes. This information is set in the |layoutMetrics|
174 - (void)gatherMetrics:(LayoutMetrics*)layoutMetrics;
176 // Support function for adjustWindowLeft:size:scrollingBy: which calculates
177 // the changes which must be applied to the folder menu window and subviews
178 // positions and sizes. |layoutMetrics| contains the proposed window size
179 // and scrolling along with the other current window and subview layout
180 // information. The values in |layoutMetrics| are then adjusted to
181 // accommodate scroll arrow presentation and window growth.
182 - (void)adjustMetrics:(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 top of the folder menu window.
187 - (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics;
189 // Support function for adjustMetrics: which calculates the layout changes
190 // required to accommodate changes in the position and scrollability
191 // of the bottom of the folder menu window.
192 - (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics;
194 // Support function for adjustWindowLeft:size:scrollingBy: which applies
195 // the layout adjustments to the folder menu window and subviews.
196 - (void)applyMetrics:(LayoutMetrics*)layoutMetrics;
198 // This function is called when buttons are added or removed from the folder
199 // menu, and which may require a change in the layout of the folder menu
200 // window. Such layout changes may include horizontal placement, width,
201 // height, and scroller visibility changes. (This function calls through
202 // to -[adjustWindowLeft:size:scrollingBy:].)
203 // |buttonCount| should contain the updated count of menu buttons.
204 - (void)adjustWindowForButtonCount:(NSUInteger)buttonCount;
206 // A helper function which takes the desired amount to scroll, given by
207 // |scrollDelta|, and calculates the actual scrolling change to be applied
208 // taking into account the layout of the folder menu window and any
209 // changes in it's scrollability. (For example, when scrolling down and the
210 // top-most menu item is coming into view we will only scroll enough for
211 // that item to be completely presented, which may be less than the
212 // scroll amount requested.)
213 - (CGFloat)determineFinalScrollDelta:(CGFloat)scrollDelta;
215 // |point| is in the base coordinate system of the destination window;
216 // it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
217 // made and inserted into the new location while leaving the bookmark in
218 // the old location, otherwise move the bookmark by removing from its old
219 // location and inserting into the new location.
220 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
226 @interface BookmarkButton (BookmarkBarFolderMenuHighlighting)
228 // Make the button's border frame always appear when |forceOn| is YES,
229 // otherwise only border the button when the mouse is inside the button.
230 - (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;
234 @implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)
236 - (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
237 [self setShowsBorderOnlyWhileMouseInside:!forceOn];
238 [self setNeedsDisplay];
243 @implementation BookmarkBarFolderController
245 @synthesize subFolderGrowthToRight = subFolderGrowthToRight_;
247 - (id)initWithParentButton:(BookmarkButton*)button
248 parentController:(BookmarkBarFolderController*)parentController
249 barController:(BookmarkBarController*)barController
250 profile:(Profile*)profile {
252 [base::mac::FrameworkBundle() pathForResource:@"BookmarkBarFolderWindow"
254 if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
255 parentButton_.reset([button retain]);
260 // We want the button to remain bordered as part of the menu path.
261 [button forceButtonBorderToStayOnAlways:YES];
263 // Pick the parent button's screen to be the screen upon which all display
264 // happens. This loop over all screens is not equivalent to
265 // |[[button window] screen]|. BookmarkButtons are commonly positioned near
266 // the edge of their windows (both in the bookmark bar and in other bookmark
267 // menus), and |[[button window] screen]| would return the screen that the
268 // majority of their window was on even if the parent button were clearly
269 // contained within a different screen.
270 NSRect parentButtonGlobalFrame =
271 [button convertRect:[button bounds] toView:nil];
272 parentButtonGlobalFrame.origin =
273 [[button window] convertBaseToScreen:parentButtonGlobalFrame.origin];
274 for (NSScreen* screen in [NSScreen screens]) {
275 if (NSIntersectsRect([screen frame], parentButtonGlobalFrame)) {
281 // The parent button is offscreen. The ideal thing to do would be to
282 // calculate the "closest" screen, the screen which has an edge parallel
283 // to, and the least distance from, one of the edges of the button.
284 // However, popping a subfolder from an offscreen button is an unrealistic
285 // edge case and so this ideal remains unrealized. Cheat instead; this
286 // code is wrong but a lot simpler.
287 screen_ = [[button window] screen];
290 parentController_.reset([parentController retain]);
291 if (!parentController_)
292 [self setSubFolderGrowthToRight:YES];
294 [self setSubFolderGrowthToRight:[parentController
295 subFolderGrowthToRight]];
296 barController_ = barController; // WEAK
297 buttons_.reset([[NSMutableArray alloc] init]);
299 [[BookmarkFolderTarget alloc] initWithController:self profile:profile]);
300 [self configureWindow];
301 hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
307 [self clearInputText];
309 // The button is no longer part of the menu path.
310 [parentButton_ forceButtonBorderToStayOnAlways:NO];
311 [parentButton_ setNeedsDisplay];
313 [self removeScrollTracking];
315 [hoverState_ draggingExited];
317 // Delegate pattern does not retain; make sure pointers to us are removed.
318 for (BookmarkButton* button in buttons_.get()) {
319 [button setDelegate:nil];
320 [button setTarget:nil];
321 [button setAction:nil];
324 // Note: we don't need to
325 // [NSObject cancelPreviousPerformRequestsWithTarget:self];
326 // Because all of our performSelector: calls use withDelay: which
331 - (void)awakeFromNib {
332 NSRect windowFrame = [[self window] frame];
333 NSRect scrollViewFrame = [scrollView_ frame];
334 padding_ = NSWidth(windowFrame) - NSWidth(scrollViewFrame);
335 verticalScrollArrowHeight_ = NSHeight([scrollUpArrowView_ frame]);
338 // Overriden from NSWindowController to call childFolderWillShow: before showing
340 - (void)showWindow:(id)sender {
341 [barController_ childFolderWillShow:self];
342 [super showWindow:sender];
346 return [[self buttons] count];
349 - (BookmarkButton*)parentButton {
350 return parentButton_.get();
353 - (void)offsetFolderMenuWindow:(NSSize)offset {
354 NSWindow* window = [self window];
355 NSRect windowFrame = [window frame];
356 windowFrame.origin.x -= offset.width;
357 windowFrame.origin.y += offset.height; // Yes, in the opposite direction!
358 [window setFrame:windowFrame display:YES];
359 [folderController_ offsetFolderMenuWindow:offset];
362 - (void)reconfigureMenu {
363 [NSObject cancelPreviousPerformRequestsWithTarget:self];
364 for (BookmarkButton* button in buttons_.get()) {
365 [button setDelegate:nil];
366 [button removeFromSuperview];
368 [buttons_ removeAllObjects];
369 [self configureWindow];
372 #pragma mark Private Methods
374 - (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
375 NSImage* image = child ? [barController_ faviconForNode:child] : nil;
376 BookmarkContextMenuCocoaController* menuController =
377 [barController_ menuController];
378 BookmarkBarFolderButtonCell* cell =
379 [BookmarkBarFolderButtonCell buttonCellForNode:child
382 menuController:menuController];
383 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
387 // Redirect to our logic shared with BookmarkBarController.
388 - (IBAction)openBookmarkFolderFromButton:(id)sender {
389 [folderTarget_ openBookmarkFolderFromButton:sender];
392 // Create a bookmark button for the given node using frame.
394 // If |node| is NULL this is an "(empty)" button.
395 // Does NOT add this button to our button list.
396 // Returns an autoreleased button.
397 // Adjusts the input frame width as appropriate.
399 // TODO(jrg): combine with addNodesToButtonList: code from
400 // bookmark_bar_controller.mm, and generalize that to use both x and y
402 // http://crbug.com/35966
403 - (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node
404 frame:(NSRect)frame {
405 BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
408 // We must decide if we draw the folder arrow before we ask the cell
409 // how big it needs to be.
410 if (node && node->is_folder()) {
411 // Warning when combining code with bookmark_bar_controller.mm:
412 // this call should NOT be made for the bar buttons; only for the
413 // subfolder buttons.
414 [cell setDrawFolderArrow:YES];
417 // The "+2" is needed because, sometimes, Cocoa is off by a tad when
418 // returning the value it thinks it needs.
419 CGFloat desired = [cell cellSize].width + 2;
420 // The width is determined from the maximum of the proposed width
421 // (provided in |frame|) or the natural width of the title, then
422 // limited by the abolute minimum and maximum allowable widths.
424 std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
425 std::max(frame.size.width, desired)),
426 bookmarks::kBookmarkMenuButtonMaximumWidth);
428 BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
432 [button setCell:cell];
433 [button setDelegate:self];
435 if (node->is_folder()) {
436 [button setTarget:self];
437 [button setAction:@selector(openBookmarkFolderFromButton:)];
439 // Make the button do something.
440 [button setTarget:barController_];
441 [button setAction:@selector(openBookmark:)];
443 [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
444 [button setAcceptsTrackIn:YES];
447 [button setEnabled:NO];
448 [button setBordered:NO];
454 return folderTarget_.get();
458 // Our parent controller is another BookmarkBarFolderController, so
459 // our window is to the right or left of it. We use a little overlap
460 // since it looks much more menu-like than with none. If we would
461 // grow off the screen, switch growth to the other direction. Growth
462 // direction sticks for folder windows which are descendents of us.
463 // If we have tried both directions and neither fits, degrade to a
465 - (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
466 // We may legitimately need to try two times (growth to right and
467 // left but not in that order). Limit us to three tries in case
468 // the folder window can't fit on either side of the screen; we
469 // don't want to loop forever.
473 // Try to grow right.
474 if ([self subFolderGrowthToRight]) {
476 x = NSMaxX([[parentButton_ window] frame]) -
477 bookmarks::kBookmarkMenuOverlap;
478 // If off the screen, switch direction.
479 if ((x + windowWidth +
480 bookmarks::kBookmarkHorizontalScreenPadding) >
481 NSMaxX([screen_ visibleFrame])) {
482 [self setSubFolderGrowthToRight:NO];
488 if (![self subFolderGrowthToRight]) {
490 x = NSMinX([[parentButton_ window] frame]) +
491 bookmarks::kBookmarkMenuOverlap -
493 // If off the screen, switch direction.
494 if (x < NSMinX([screen_ visibleFrame])) {
495 [self setSubFolderGrowthToRight:YES];
501 // Unhappy; do the best we can.
502 return NSMaxX([screen_ visibleFrame]) - windowWidth;
506 // Compute and return the top left point of our window (screen
507 // coordinates). The top left is positioned in a manner similar to
508 // cascading menus. Windows may grow to either the right or left of
509 // their parent (if a sub-folder) so we need to know |windowWidth|.
510 - (NSPoint)windowTopLeftForWidth:(int)windowWidth height:(int)windowHeight {
511 CGFloat kMinSqueezedMenuHeight = bookmarks::kBookmarkFolderButtonHeight * 2.0;
512 NSPoint newWindowTopLeft;
513 if (![parentController_ isKindOfClass:[self class]]) {
514 // If we're not popping up from one of ourselves, we must be
515 // popping up from the bookmark bar itself. In this case, start
516 // BELOW the parent button. Our left is the button left; our top
517 // is bottom of button's parent view.
518 NSPoint buttonBottomLeftInScreen =
519 [[parentButton_ window]
520 convertBaseToScreen:[parentButton_
521 convertPoint:NSZeroPoint toView:nil]];
522 NSPoint bookmarkBarBottomLeftInScreen =
523 [[parentButton_ window]
524 convertBaseToScreen:[[parentButton_ superview]
525 convertPoint:NSZeroPoint toView:nil]];
526 newWindowTopLeft = NSMakePoint(
527 buttonBottomLeftInScreen.x + bookmarks::kBookmarkBarButtonOffset,
528 bookmarkBarBottomLeftInScreen.y + bookmarks::kBookmarkBarMenuOffset);
529 // Make sure the window is on-screen; if not, push left. It is
530 // intentional that top level folders "push left" slightly
531 // different than subfolders.
532 NSRect screenFrame = [screen_ visibleFrame];
533 CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
534 if (spillOff > 0.0) {
535 newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
536 NSMinX(screenFrame));
538 // The menu looks bad when it is squeezed up against the bottom of the
539 // screen and ends up being only a few pixels tall. If it meets the
540 // threshold for this case, instead show the menu above the button.
541 CGFloat availableVerticalSpace = newWindowTopLeft.y -
542 (NSMinY(screenFrame) + bookmarks::kScrollWindowVerticalMargin);
543 if ((availableVerticalSpace < kMinSqueezedMenuHeight) &&
544 (windowHeight > availableVerticalSpace)) {
545 newWindowTopLeft.y = std::min(
546 newWindowTopLeft.y + windowHeight + NSHeight([parentButton_ frame]),
547 NSMaxY(screenFrame));
550 // Parent is a folder: expose as much as we can vertically; grow right/left.
551 newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
552 NSPoint topOfWindow = NSMakePoint(0,
553 NSMaxY([parentButton_ frame]) -
554 bookmarks::kBookmarkVerticalPadding);
555 topOfWindow = [[parentButton_ window]
556 convertBaseToScreen:[[parentButton_ superview]
557 convertPoint:topOfWindow toView:nil]];
558 newWindowTopLeft.y = topOfWindow.y +
559 2 * bookmarks::kBookmarkVerticalPadding;
561 return newWindowTopLeft;
564 // Set our window level to the right spot so we're above the menubar, dock, etc.
565 // Factored out so we can override/noop in a unit test.
566 - (void)configureWindowLevel {
567 [[self window] setLevel:NSPopUpMenuWindowLevel];
570 - (int)menuHeightForButtonCount:(int)buttonCount {
571 // This does not take into account any padding which may be required at the
572 // top and/or bottom of the window.
573 return (buttonCount * bookmarks::kBookmarkFolderButtonHeight) +
574 2 * bookmarks::kBookmarkVerticalPadding;
577 - (void)adjustWindowLeft:(CGFloat)windowLeft
578 size:(NSSize)windowSize
579 scrollingBy:(CGFloat)scrollDelta {
580 // Callers of this function should make adjustments to the vertical
581 // attributes of the folder view only (height, scroll position).
582 // This function will then make appropriate layout adjustments in order
583 // to accommodate screen/dock margins, scroll-up and scroll-down arrow
584 // presentation, etc.
585 // The 4 views whose vertical height and origins may be adjusted
586 // by this function are:
587 // 1) window, 2) visible content view, 3) scroller view, 4) folder view.
589 LayoutMetrics layoutMetrics(windowLeft, windowSize, scrollDelta);
590 [self gatherMetrics:&layoutMetrics];
591 [self adjustMetrics:&layoutMetrics];
592 [self applyMetrics:&layoutMetrics];
595 - (void)gatherMetrics:(LayoutMetrics*)layoutMetrics {
596 LayoutMetrics& metrics(*layoutMetrics);
597 NSWindow* window = [self window];
598 metrics.windowFrame = [window frame];
599 metrics.visibleFrame = [visibleView_ frame];
600 metrics.scrollerFrame = [scrollView_ frame];
601 metrics.scrollPoint = [scrollView_ documentVisibleRect].origin;
602 metrics.scrollPoint.y -= metrics.scrollDelta;
603 metrics.couldScrollUp = ![scrollUpArrowView_ isHidden];
604 metrics.couldScrollDown = ![scrollDownArrowView_ isHidden];
606 metrics.deltaWindowHeight = 0.0;
607 metrics.deltaWindowY = 0.0;
608 metrics.deltaVisibleHeight = 0.0;
609 metrics.deltaVisibleY = 0.0;
610 metrics.deltaScrollerHeight = 0.0;
611 metrics.deltaScrollerY = 0.0;
613 metrics.minimumY = NSMinY([screen_ visibleFrame]) +
614 bookmarks::kScrollWindowVerticalMargin;
615 metrics.screenBottomY = NSMinY([screen_ frame]);
616 metrics.oldWindowY = NSMinY(metrics.windowFrame);
618 metrics.scrollerFrame.origin.y + metrics.visibleFrame.origin.y +
619 metrics.oldWindowY - metrics.scrollPoint.y;
620 metrics.folderTop = metrics.folderY + NSHeight([folderView_ frame]);
623 - (void)adjustMetrics:(LayoutMetrics*)layoutMetrics {
624 LayoutMetrics& metrics(*layoutMetrics);
625 CGFloat effectiveFolderY = metrics.folderY;
626 if (!metrics.couldScrollUp && !metrics.couldScrollDown)
627 effectiveFolderY -= metrics.windowSize.height;
628 metrics.canScrollUp = effectiveFolderY < metrics.minimumY;
630 NSMaxY([screen_ visibleFrame]) - bookmarks::kScrollWindowVerticalMargin;
631 metrics.canScrollDown = metrics.folderTop > maximumY;
633 // Accommodate changes in the bottom of the menu.
634 [self adjustMetricsForMenuBottomChanges:layoutMetrics];
636 // Accommodate changes in the top of the menu.
637 [self adjustMetricsForMenuTopChanges:layoutMetrics];
639 metrics.scrollerFrame.origin.y += metrics.deltaScrollerY;
640 metrics.scrollerFrame.size.height += metrics.deltaScrollerHeight;
641 metrics.visibleFrame.origin.y += metrics.deltaVisibleY;
642 metrics.visibleFrame.size.height += metrics.deltaVisibleHeight;
643 metrics.preScroll = metrics.canScrollUp && !metrics.couldScrollUp &&
644 metrics.scrollDelta == 0.0 && metrics.deltaWindowHeight >= 0.0;
645 metrics.windowFrame.origin.y += metrics.deltaWindowY;
646 metrics.windowFrame.origin.x = metrics.windowLeft;
647 metrics.windowFrame.size.height += metrics.deltaWindowHeight;
648 metrics.windowFrame.size.width = metrics.windowSize.width;
651 - (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics {
652 LayoutMetrics& metrics(*layoutMetrics);
653 if (metrics.canScrollUp) {
654 if (!metrics.couldScrollUp) {
656 metrics.deltaWindowY = metrics.screenBottomY - metrics.oldWindowY;
657 metrics.deltaWindowHeight = -metrics.deltaWindowY;
658 metrics.deltaVisibleY = metrics.minimumY - metrics.screenBottomY;
659 metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
660 metrics.deltaScrollerY = verticalScrollArrowHeight_;
661 metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
662 // Adjust the scroll delta if we've grown the window and it is
663 // now scroll-up-able, but don't adjust it if we've
664 // scrolled down and it wasn't scroll-up-able but now is.
665 if (metrics.canScrollDown == metrics.couldScrollDown) {
666 CGFloat deltaScroll = metrics.deltaWindowY - metrics.screenBottomY +
667 metrics.deltaScrollerY + metrics.deltaVisibleY;
668 metrics.scrollPoint.y += deltaScroll + metrics.windowSize.height;
670 } else if (!metrics.canScrollDown && metrics.windowSize.height > 0.0) {
671 metrics.scrollPoint.y += metrics.windowSize.height;
674 if (metrics.couldScrollUp) {
676 metrics.deltaWindowY = metrics.folderY - metrics.oldWindowY;
677 metrics.deltaWindowHeight = -metrics.deltaWindowY;
678 metrics.deltaVisibleY = -metrics.visibleFrame.origin.y;
679 metrics.deltaVisibleHeight = -metrics.deltaVisibleY;
680 metrics.deltaScrollerY = -verticalScrollArrowHeight_;
681 metrics.deltaScrollerHeight = -metrics.deltaScrollerY;
682 // We are no longer scroll-up-able so the scroll point drops to zero.
683 metrics.scrollPoint.y = 0.0;
686 // Check for menu height change by looking at the relative tops of the
687 // menu folder and the window folder, which previously would have been
689 metrics.deltaWindowY = NSMaxY(metrics.windowFrame) - metrics.folderTop;
690 metrics.deltaWindowHeight = -metrics.deltaWindowY;
695 - (void)adjustMetricsForMenuTopChanges:(LayoutMetrics*)layoutMetrics {
696 LayoutMetrics& metrics(*layoutMetrics);
697 if (metrics.canScrollDown == metrics.couldScrollDown) {
698 if (!metrics.canScrollDown) {
699 // Not scroll-down-able but the menu top has changed.
700 metrics.deltaWindowHeight += metrics.scrollDelta;
703 if (metrics.canScrollDown) {
705 const CGFloat maximumY = NSMaxY([screen_ visibleFrame]);
706 metrics.deltaWindowHeight += (maximumY - NSMaxY(metrics.windowFrame));
707 metrics.deltaVisibleHeight -= bookmarks::kScrollWindowVerticalMargin;
708 metrics.deltaScrollerHeight -= verticalScrollArrowHeight_;
711 metrics.deltaWindowHeight -= bookmarks::kScrollWindowVerticalMargin;
712 metrics.deltaVisibleHeight += bookmarks::kScrollWindowVerticalMargin;
713 metrics.deltaScrollerHeight += verticalScrollArrowHeight_;
718 - (void)applyMetrics:(LayoutMetrics*)layoutMetrics {
719 LayoutMetrics& metrics(*layoutMetrics);
720 // Hide or show the scroll arrows.
721 if (metrics.canScrollUp != metrics.couldScrollUp)
722 [scrollUpArrowView_ setHidden:metrics.couldScrollUp];
723 if (metrics.canScrollDown != metrics.couldScrollDown)
724 [scrollDownArrowView_ setHidden:metrics.couldScrollDown];
726 // Adjust the geometry. The order is important because of sizer dependencies.
727 [scrollView_ setFrame:metrics.scrollerFrame];
728 [visibleView_ setFrame:metrics.visibleFrame];
729 // This little bit of trickery handles the one special case where
730 // the window is now scroll-up-able _and_ going to be resized -- scroll
731 // first in order to prevent flashing.
732 if (metrics.preScroll)
733 [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
735 [[self window] setFrame:metrics.windowFrame display:YES];
737 // In all other cases we defer scrolling until the window has been resized
738 // in order to prevent flashing.
739 if (!metrics.preScroll)
740 [[scrollView_ documentView] scrollPoint:metrics.scrollPoint];
742 // TODO(maf) find a non-SPI way to do this.
743 // Hack. This is the only way I've found to get the tracking area cache
744 // to update properly during a mouse tracking loop.
745 // Without this, the item tracking-areas are wrong when using a scrollable
746 // menu with the mouse held down.
747 NSView *contentView = [[self window] contentView] ;
748 if ([contentView respondsToSelector:@selector(_updateTrackingAreas)])
749 [contentView _updateTrackingAreas];
752 if (metrics.canScrollUp != metrics.couldScrollUp ||
753 metrics.canScrollDown != metrics.couldScrollDown ||
754 metrics.scrollDelta != 0.0) {
755 if (metrics.canScrollUp || metrics.canScrollDown)
756 [self addOrUpdateScrollTracking];
758 [self removeScrollTracking];
762 - (void)adjustWindowForButtonCount:(NSUInteger)buttonCount {
763 NSRect folderFrame = [folderView_ frame];
764 CGFloat newMenuHeight =
765 (CGFloat)[self menuHeightForButtonCount:buttonCount];
766 CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame);
767 // If the height has changed then also change the origin, and adjust the
768 // scroll (if scrolling).
769 if ([self canScrollUp]) {
770 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
771 scrollPoint.y += deltaMenuHeight;
772 [[scrollView_ documentView] scrollPoint:scrollPoint];
774 folderFrame.size.height += deltaMenuHeight;
775 [folderView_ setFrameSize:folderFrame.size];
776 CGFloat windowWidth = [self adjustButtonWidths] + padding_;
777 NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
778 height:deltaMenuHeight];
779 CGFloat left = newWindowTopLeft.x;
780 NSSize newSize = NSMakeSize(windowWidth, deltaMenuHeight);
781 [self adjustWindowLeft:left size:newSize scrollingBy:0.0];
784 // Determine window size and position.
785 // Create buttons for all our nodes.
786 // TODO(jrg): break up into more and smaller routines for easier unit testing.
787 - (void)configureWindow {
788 const BookmarkNode* node = [parentButton_ bookmarkNode];
790 int startingIndex = [[parentButton_ cell] startingChildIndex];
791 DCHECK_LE(startingIndex, node->child_count());
792 // Must have at least 1 button (for "empty")
793 int buttons = std::max(node->child_count() - startingIndex, 1);
795 // Prelim height of the window. We'll trim later as needed.
796 int height = [self menuHeightForButtonCount:buttons];
797 // We'll need this soon...
800 // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
801 // http://crbug.com/35966
802 NSRect buttonsOuterFrame = GetFirstButtonFrameForHeight(height);
804 // TODO(jrg): combine with addNodesToButtonList: code from
805 // bookmark_bar_controller.mm (but use y offset)
806 // http://crbug.com/35966
808 // If no children we are the empty button.
809 BookmarkButton* button = [self makeButtonForNode:nil
810 frame:buttonsOuterFrame];
811 [buttons_ addObject:button];
812 [folderView_ addSubview:button];
814 for (int i = startingIndex; i < node->child_count(); ++i) {
815 const BookmarkNode* child = node->GetChild(i);
816 BookmarkButton* button = [self makeButtonForNode:child
817 frame:buttonsOuterFrame];
818 [buttons_ addObject:button];
819 [folderView_ addSubview:button];
820 buttonsOuterFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
823 [self layOutWindowWithHeight:height];
826 - (void)layOutWindowWithHeight:(CGFloat)height {
827 // Lay out the window by adjusting all button widths to be consistent, then
828 // base the window width on this ideal button width.
829 CGFloat buttonWidth = [self adjustButtonWidths];
830 CGFloat windowWidth = buttonWidth + padding_;
831 NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth
834 // Make sure as much of a submenu is exposed (which otherwise would be a
835 // problem if the parent button is close to the bottom of the screen).
836 if ([parentController_ isKindOfClass:[self class]]) {
837 CGFloat minimumY = NSMinY([screen_ visibleFrame]) +
838 bookmarks::kScrollWindowVerticalMargin +
840 newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY);
843 NSWindow* window = [self window];
844 NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
845 newWindowTopLeft.y - height,
846 windowWidth, height);
847 [window setFrame:windowFrame display:NO];
849 NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height);
850 [folderView_ setFrame:folderFrame];
852 // For some reason, when opening a "large" bookmark folder (containing 12 or
853 // more items) using the keyboard, the scroll view seems to want to be
854 // offset by default: [ http://crbug.com/101099 ]. Explicitly reseting the
855 // scroll position here is a bit hacky, but it does seem to work.
856 [[scrollView_ contentView] scrollToPoint:NSZeroPoint];
858 NSSize newSize = NSMakeSize(windowWidth, 0.0);
859 [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0];
860 [self configureWindowLevel];
865 // TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
866 - (CGFloat)adjustButtonWidths {
867 CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
868 // Use the cell's size as the base for determining the desired width of the
869 // button rather than the button's current width. -[cell cellSize] always
870 // returns the 'optimum' size of the cell based on the cell's contents even
871 // if it's less than the current button size. Relying on the button size
872 // would result in buttons that could only get wider but we want to handle
873 // the case where the widest button gets removed from a folder menu.
874 for (BookmarkButton* button in buttons_.get())
875 width = std::max(width, [[button cell] cellSize].width);
876 width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
877 // Things look and feel more menu-like if all the buttons are the
878 // full width of the window, especially if there are submenus.
879 for (BookmarkButton* button in buttons_.get()) {
880 NSRect buttonFrame = [button frame];
881 buttonFrame.size.width = width;
882 [button setFrame:buttonFrame];
887 // Start a "scroll up" timer.
888 - (void)beginScrollWindowUp {
889 [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
892 // Start a "scroll down" timer.
893 - (void)beginScrollWindowDown {
894 [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
897 // End a scrolling timer. Can be called excessively with no harm.
900 [scrollTimer_ invalidate];
902 verticalScrollDelta_ = 0;
906 - (int)indexOfButton:(BookmarkButton*)button {
909 NSInteger index = [buttons_ indexOfObject:button];
910 return (index == NSNotFound) ? -1 : index;
913 - (BookmarkButton*)buttonAtIndex:(int)which {
914 if (which < 0 || which >= [self buttonCount])
916 return [buttons_ objectAtIndex:which];
919 // Private, called by performOneScroll only.
920 // If the button at index contains the mouse it will select it and return YES.
921 // Otherwise returns NO.
922 - (BOOL)selectButtonIfHoveredAtIndex:(int)index {
923 BookmarkButton* button = [self buttonAtIndex:index];
924 if ([[button cell] isMouseReallyInside]) {
925 buttonThatMouseIsIn_ = button;
926 [self setSelectedButtonByIndex:index];
932 // Perform a single scroll of the specified amount.
933 - (void)performOneScroll:(CGFloat)delta {
936 CGFloat finalDelta = [self determineFinalScrollDelta:delta];
937 if (finalDelta == 0.0)
939 int index = [self indexOfButton:buttonThatMouseIsIn_];
940 // Check for a current mouse-initiated selection.
941 BOOL maintainHoverSelection =
942 (buttonThatMouseIsIn_ &&
943 [[buttonThatMouseIsIn_ cell] isMouseReallyInside] &&
944 selectedIndex_ != -1 &&
945 index == selectedIndex_);
946 NSRect windowFrame = [[self window] frame];
947 NSSize newSize = NSMakeSize(NSWidth(windowFrame), 0.0);
948 [self adjustWindowLeft:windowFrame.origin.x
950 scrollingBy:finalDelta];
951 // We have now scrolled.
952 if (!maintainHoverSelection)
954 // Is mouse still in the same hovered button?
955 if ([[buttonThatMouseIsIn_ cell] isMouseReallyInside])
957 // The finalDelta scroll direction will tell us us whether to search up or
958 // down the buttons array for the newly hovered button.
959 if (finalDelta < 0.0) { // Scrolled up, so search backwards for new hover.
962 if ([self selectButtonIfHoveredAtIndex:index])
966 } else { // Scrolled down, so search forward for new hovered button.
968 int btnMax = [self buttonCount];
969 while (index < btnMax) {
970 if ([self selectButtonIfHoveredAtIndex:index])
977 - (CGFloat)determineFinalScrollDelta:(CGFloat)delta {
978 if ((delta > 0.0 && ![scrollUpArrowView_ isHidden]) ||
979 (delta < 0.0 && ![scrollDownArrowView_ isHidden])) {
980 NSWindow* window = [self window];
981 NSRect windowFrame = [window frame];
982 NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
983 CGFloat scrollY = scrollPosition.y;
984 NSRect scrollerFrame = [scrollView_ frame];
985 CGFloat scrollerY = NSMinY(scrollerFrame);
986 NSRect visibleFrame = [visibleView_ frame];
987 CGFloat visibleY = NSMinY(visibleFrame);
988 CGFloat windowY = NSMinY(windowFrame);
989 CGFloat offset = scrollerY + visibleY + windowY;
993 CGFloat minimumY = NSMinY([screen_ visibleFrame]) +
994 bookmarks::kScrollWindowVerticalMargin;
995 CGFloat maxUpDelta = scrollY - offset + minimumY;
996 delta = MIN(delta, maxUpDelta);
999 NSRect screenFrame = [screen_ visibleFrame];
1000 CGFloat topOfScreen = NSMaxY(screenFrame);
1001 NSRect folderFrame = [folderView_ frame];
1002 CGFloat folderHeight = NSHeight(folderFrame);
1003 CGFloat folderTop = folderHeight - scrollY + offset;
1004 CGFloat maxDownDelta =
1005 topOfScreen - folderTop - bookmarks::kScrollWindowVerticalMargin;
1006 delta = MAX(delta, maxDownDelta);
1014 // Perform a scroll of the window on the screen.
1015 // Called by a timer when scrolling.
1016 - (void)performScroll:(NSTimer*)timer {
1017 DCHECK(verticalScrollDelta_);
1018 [self performOneScroll:verticalScrollDelta_];
1022 // Add a timer to fire at a regular interval which scrolls the
1023 // window vertically |delta|.
1024 - (void)addScrollTimerWithDelta:(CGFloat)delta {
1025 if (scrollTimer_ && verticalScrollDelta_ == delta)
1028 verticalScrollDelta_ = delta;
1029 scrollTimer_ = [NSTimer timerWithTimeInterval:kBookmarkBarFolderScrollInterval
1031 selector:@selector(performScroll:)
1035 [[NSRunLoop mainRunLoop] addTimer:scrollTimer_ forMode:NSRunLoopCommonModes];
1039 // Called as a result of our tracking area. Warning: on the main
1040 // screen (of a single-screened machine), the minimum mouse y value is
1041 // 1, not 0. Also, we do not get events when the mouse is above the
1042 // menubar (to be fixed by setting the proper window level; see
1044 // Note [theEvent window] may not be our window, as we also get these messages
1045 // forwarded from BookmarkButton's mouse tracking loop.
1046 - (void)mouseMovedOrDragged:(NSEvent*)theEvent {
1047 NSPoint eventScreenLocation =
1048 [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]];
1050 // Base hot spot calculations on the positions of the scroll arrow views.
1051 NSRect testRect = [scrollDownArrowView_ frame];
1052 NSPoint testPoint = [visibleView_ convertPoint:testRect.origin
1054 testPoint = [[self window] convertBaseToScreen:testPoint];
1055 CGFloat closeToTopOfScreen = testPoint.y;
1057 testRect = [scrollUpArrowView_ frame];
1058 testPoint = [visibleView_ convertPoint:testRect.origin toView:nil];
1059 testPoint = [[self window] convertBaseToScreen:testPoint];
1060 CGFloat closeToBottomOfScreen = testPoint.y + testRect.size.height;
1061 if (eventScreenLocation.y <= closeToBottomOfScreen &&
1062 ![scrollUpArrowView_ isHidden]) {
1063 [self beginScrollWindowUp];
1064 } else if (eventScreenLocation.y > closeToTopOfScreen &&
1065 ![scrollDownArrowView_ isHidden]) {
1066 [self beginScrollWindowDown];
1072 - (void)mouseMoved:(NSEvent*)theEvent {
1073 [self mouseMovedOrDragged:theEvent];
1076 - (void)mouseDragged:(NSEvent*)theEvent {
1077 [self mouseMovedOrDragged:theEvent];
1080 - (void)mouseExited:(NSEvent*)theEvent {
1084 // Add a tracking area so we know when the mouse is pinned to the top
1085 // or bottom of the screen. If that happens, and if the mouse
1086 // position overlaps the window, scroll it.
1087 - (void)addOrUpdateScrollTracking {
1088 [self removeScrollTracking];
1089 NSView* view = [[self window] contentView];
1090 scrollTrackingArea_.reset([[CrTrackingArea alloc]
1091 initWithRect:[view bounds]
1092 options:(NSTrackingMouseMoved |
1093 NSTrackingMouseEnteredAndExited |
1094 NSTrackingActiveAlways |
1095 NSTrackingEnabledDuringMouseDrag
1099 [view addTrackingArea:scrollTrackingArea_.get()];
1102 // Remove the tracking area associated with scrolling.
1103 - (void)removeScrollTracking {
1104 if (scrollTrackingArea_.get()) {
1105 [[[self window] contentView] removeTrackingArea:scrollTrackingArea_.get()];
1106 [scrollTrackingArea_.get() clearOwner];
1108 scrollTrackingArea_.reset();
1111 // Close the old hover-open bookmark folder, and open a new one. We
1112 // do both in one step to allow for a delay in closing the old one.
1113 // See comments above kDragHoverCloseDelay (bookmark_bar_controller.h)
1114 // for more details.
1115 - (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender {
1116 // Ignore if sender button is in a window that's just been hidden - that
1117 // would leave us with an orphaned menu. BUG 69002
1118 if ([[sender window] isVisible] != YES)
1120 // If an old submenu exists, close it immediately.
1121 [self closeBookmarkFolder:sender];
1123 // Open a new one if meaningful.
1124 if ([sender isFolder])
1125 [folderTarget_ openBookmarkFolderFromButton:sender];
1128 - (NSArray*)buttons {
1129 return buttons_.get();
1133 [folderController_ close];
1137 - (void)scrollWheel:(NSEvent *)theEvent {
1138 if (![scrollUpArrowView_ isHidden] || ![scrollDownArrowView_ isHidden]) {
1139 // We go negative since an NSScrollView has a flipped coordinate frame.
1140 CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY];
1141 [self performOneScroll:amt];
1145 #pragma mark Drag & Drop
1147 // Find something like std::is_between<T>? I can't believe one doesn't exist.
1148 // http://crbug.com/35966
1149 static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
1150 return ((value >= low) && (value <= high));
1153 // Return the proposed drop target for a hover open button, or nil if none.
1155 // TODO(jrg): this is just like the version in
1156 // bookmark_bar_controller.mm, but vertical instead of horizontal.
1157 // Generalize to be axis independent then share code.
1158 // http://crbug.com/35966
1159 - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point {
1160 NSPoint localPoint = [folderView_ convertPoint:point fromView:nil];
1161 for (BookmarkButton* button in buttons_.get()) {
1162 // No early break -- makes no assumption about button ordering.
1164 // Intentionally NOT using NSPointInRect() so that scrolling into
1165 // a submenu doesn't cause it to be closed.
1166 if (ValueInRangeInclusive(NSMinY([button frame]),
1168 NSMaxY([button frame]))) {
1170 // Over a button but let's be a little more specific
1171 // (e.g. over the middle half).
1172 NSRect frame = [button frame];
1173 NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4);
1174 if (ValueInRangeInclusive(NSMinY(middleHalfOfButton),
1176 NSMaxY(middleHalfOfButton))) {
1177 // It makes no sense to drop on a non-folder; there is no hover.
1178 if (![button isFolder])
1183 // Over a button but not over the middle half.
1188 // Not hovering over a button.
1192 // TODO(jrg): again we have code dup, sort of, with
1193 // bookmark_bar_controller.mm, but the axis is changed. One minor
1194 // difference is accomodation for the "empty" button (which may not
1195 // exist in the future).
1196 // http://crbug.com/35966
1197 - (int)indexForDragToPoint:(NSPoint)point {
1198 // Identify which buttons we are between. For now, assume a button
1199 // location is at the center point of its view, and that an exact
1200 // match means "place before".
1201 // TODO(jrg): revisit position info based on UI team feedback.
1202 // dropLocation is in bar local coordinates.
1203 // http://crbug.com/36276
1204 NSPoint dropLocation =
1205 [folderView_ convertPoint:point
1206 fromView:[[self window] contentView]];
1207 BookmarkButton* buttonToTheTopOfDraggedButton = nil;
1208 // Buttons are laid out in this array from top to bottom (screen
1209 // wise), which means "biggest y" --> "smallest y".
1210 for (BookmarkButton* button in buttons_.get()) {
1211 CGFloat midpoint = NSMidY([button frame]);
1212 if (dropLocation.y > midpoint) {
1215 buttonToTheTopOfDraggedButton = button;
1218 // TODO(jrg): On Windows, dropping onto (empty) highlights the
1219 // entire drop location and does not use an insertion point.
1220 // http://crbug.com/35967
1221 if (!buttonToTheTopOfDraggedButton) {
1222 // We are at the very top (we broke out of the loop on the first try).
1225 if ([buttonToTheTopOfDraggedButton isEmpty]) {
1226 // There is a button but it's an empty placeholder.
1227 // Default to inserting on top of it.
1230 const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton
1233 // Be careful if the number of buttons != number of nodes.
1234 return ((beforeNode->parent()->GetIndexOf(beforeNode) + 1) -
1235 [[parentButton_ cell] startingChildIndex]);
1238 // TODO(jrg): Yet more code dup.
1239 // http://crbug.com/35966
1240 - (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
1245 // Drop destination.
1246 const BookmarkNode* destParent = NULL;
1249 // First check if we're dropping on a button. If we have one, and
1250 // it's a folder, drop in it.
1251 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1252 if ([button isFolder]) {
1253 destParent = [button bookmarkNode];
1254 // Drop it at the end.
1255 destIndex = [button bookmarkNode]->child_count();
1257 // Else we're dropping somewhere in the folder, so find the right spot.
1258 destParent = [parentButton_ bookmarkNode];
1259 destIndex = [self indexForDragToPoint:point];
1260 // Be careful if the number of buttons != number of nodes.
1261 destIndex += [[parentButton_ cell] startingChildIndex];
1264 ChromeBookmarkClient* client =
1265 ChromeBookmarkClientFactory::GetForProfile(profile_);
1266 if (!client->CanBeEditedByUser(destParent))
1268 if (!client->CanBeEditedByUser(sourceNode))
1272 BOOL wasCopiedOrMoved = NO;
1273 if (!destParent->HasAncestor(sourceNode)) {
1275 [self bookmarkModel]->Copy(sourceNode, destParent, destIndex);
1277 [self bookmarkModel]->Move(sourceNode, destParent, destIndex);
1278 wasCopiedOrMoved = YES;
1279 // Movement of a node triggers observers (like us) to rebuild the
1280 // bar so we don't have to do so explicitly.
1283 return wasCopiedOrMoved;
1286 // TODO(maf): Implement live drag & drop animation using this hook.
1287 - (void)setDropInsertionPos:(CGFloat)where {
1290 // TODO(maf): Implement live drag & drop animation using this hook.
1291 - (void)clearDropInsertionPos {
1294 #pragma mark NSWindowDelegate Functions
1296 - (void)windowWillClose:(NSNotification*)notification {
1297 // Also done by the dealloc method, but also doing it here is quicker and
1299 [parentButton_ forceButtonBorderToStayOnAlways:NO];
1301 // If a "hover open" is pending when the bookmark bar folder is
1302 // closed, be sure it gets cancelled.
1303 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1305 [self endScroll]; // Just in case we were scrolling.
1306 [barController_ childFolderWillClose:self];
1307 [self closeBookmarkFolder:self];
1311 #pragma mark BookmarkButtonDelegate Protocol
1313 - (void)fillPasteboard:(NSPasteboard*)pboard
1314 forDragOfButton:(BookmarkButton*)button {
1315 [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
1317 // Close our folder menu and submenus since we know we're going to be dragged.
1318 [self closeBookmarkFolder:self];
1321 // Called from BookmarkButton.
1322 // Unlike bookmark_bar_controller's version, we DO default to being enabled.
1323 - (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
1324 [[NSCursor arrowCursor] set];
1326 buttonThatMouseIsIn_ = sender;
1327 [self setSelectedButtonByIndex:[self indexOfButton:sender]];
1329 // Cancel a previous hover if needed.
1330 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1332 // If already opened, then we exited but re-entered the button
1333 // (without entering another button open), do nothing.
1334 if ([folderController_ parentButton] == sender)
1337 // If right click was done immediately on entering a button, then open the
1338 // folder without delay so that context menu appears over the folder menu.
1339 if ([event type] == NSRightMouseDown)
1340 [self openBookmarkFolderFromButtonAndCloseOldOne:sender];
1342 [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:)
1344 afterDelay:bookmarks::kHoverOpenDelay
1345 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
1348 // Called from the BookmarkButton
1349 - (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
1350 if (buttonThatMouseIsIn_ == sender)
1351 buttonThatMouseIsIn_ = nil;
1352 [self setSelectedButtonByIndex:-1];
1354 // Stop any timer about opening a new hover-open folder.
1356 // Since a performSelector:withDelay: on self retains self, it is
1357 // possible that a cancelPreviousPerformRequestsWithTarget: reduces
1358 // the refcount to 0, releasing us. That's a bad thing to do while
1359 // this object (or others it may own) is in the event chain. Thus
1360 // we have a retain/autorelease.
1362 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1366 - (NSWindow*)browserWindow {
1367 return [barController_ browserWindow];
1370 - (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
1371 return [barController_ canEditBookmarks] &&
1372 [barController_ canEditBookmark:[button bookmarkNode]];
1375 - (void)didDragBookmarkToTrash:(BookmarkButton*)button {
1376 [barController_ didDragBookmarkToTrash:button];
1379 - (void)bookmarkDragDidEnd:(BookmarkButton*)button
1380 operation:(NSDragOperation)operation {
1381 [barController_ bookmarkDragDidEnd:button
1382 operation:operation];
1386 #pragma mark BookmarkButtonControllerProtocol
1388 // Recursively close all bookmark folders.
1389 - (void)closeAllBookmarkFolders {
1390 // Closing the top level implicitly closes all children.
1391 [barController_ closeAllBookmarkFolders];
1394 // Close our bookmark folder (a sub-controller) if we have one.
1395 - (void)closeBookmarkFolder:(id)sender {
1396 if (folderController_) {
1397 // Make this menu key, so key status doesn't go back to the browser
1398 // window when the submenu closes.
1399 [[self window] makeKeyWindow];
1400 [self setSubFolderGrowthToRight:YES];
1401 [[folderController_ window] close];
1402 folderController_ = nil;
1406 - (BookmarkModel*)bookmarkModel {
1407 return [barController_ bookmarkModel];
1410 - (BOOL)draggingAllowed:(id<NSDraggingInfo>)info {
1411 return [barController_ draggingAllowed:info];
1414 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1415 // Most of the work (e.g. drop indicator) is taken care of in the
1416 // folder_view. Here we handle hover open issues for subfolders.
1417 // Caution: there are subtle differences between this one and
1418 // bookmark_bar_controller.mm's version.
1419 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
1420 NSPoint currentLocation = [info draggingLocation];
1421 BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
1423 // Don't allow drops that would result in cycles.
1425 NSData* data = [[info draggingPasteboard]
1426 dataForType:kBookmarkButtonDragType];
1427 if (data && [info draggingSource]) {
1428 BookmarkButton* sourceButton = nil;
1429 [data getBytes:&sourceButton length:sizeof(sourceButton)];
1430 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1431 const BookmarkNode* destNode = [button bookmarkNode];
1432 if (destNode->HasAncestor(sourceNode))
1436 // Delegate handling of dragging over a button to the |hoverState_| member.
1437 return [hoverState_ draggingEnteredButton:button];
1440 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info {
1441 return NSDragOperationMove;
1444 // Unlike bookmark_bar_controller, we need to keep track of dragging state.
1445 // We also need to make sure we cancel the delayed hover close.
1446 - (void)draggingExited:(id<NSDraggingInfo>)info {
1447 // NOT the same as a cancel --> we may have moved the mouse into the submenu.
1448 // Delegate handling of the hover button to the |hoverState_| member.
1449 [hoverState_ draggingExited];
1452 - (BOOL)dragShouldLockBarVisibility {
1453 return [parentController_ dragShouldLockBarVisibility];
1456 // TODO(jrg): ARGH more code dup.
1457 // http://crbug.com/35966
1458 - (BOOL)dragButton:(BookmarkButton*)sourceButton
1461 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
1462 const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
1463 return [self dragBookmark:sourceNode to:point copy:copy];
1466 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1467 // http://crbug.com/35966
1468 - (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
1470 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
1472 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
1473 NSPoint dropPoint = [info draggingLocation];
1474 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
1475 it != nodes.end(); ++it) {
1476 const BookmarkNode* sourceNode = *it;
1477 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
1483 // TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
1484 // http://crbug.com/35966
1485 - (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
1486 std::vector<const BookmarkNode*> dragDataNodes;
1487 BookmarkNodeData dragData;
1488 if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) {
1489 BookmarkModel* bookmarkModel = [self bookmarkModel];
1490 std::vector<const BookmarkNode*> nodes(
1491 dragData.GetNodes(bookmarkModel, profile_->GetPath()));
1492 dragDataNodes.assign(nodes.begin(), nodes.end());
1494 return dragDataNodes;
1497 // Return YES if we should show the drop indicator, else NO.
1498 // TODO(jrg): ARGH code dup!
1499 // http://crbug.com/35966
1500 - (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
1501 return ![self buttonForDroppingOnAtPoint:point];
1504 // Button selection change code to support type to select and arrow key events.
1505 #pragma mark Keyboard Support
1507 // Scroll the menu to show the selected button, if it's not already visible.
1508 - (void)showSelectedButton {
1509 int bMaxIndex = [self buttonCount] - 1; // Max array index in button array.
1511 // Is there a valid selected button?
1512 if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex)
1515 // Is the menu scrollable anyway?
1516 if (![self canScrollUp] && ![self canScrollDown])
1519 // Now check to see if we need to scroll, which way, and how far.
1520 CGFloat delta = 0.0;
1521 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin;
1522 CGFloat itemBottom = (bMaxIndex - selectedIndex_) *
1523 bookmarks::kBookmarkFolderButtonHeight;
1524 CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight;
1525 CGFloat viewHeight = NSHeight([scrollView_ frame]);
1527 if (scrollPoint.y > itemBottom) { // Need to scroll down.
1528 delta = scrollPoint.y - itemBottom;
1529 } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up.
1530 delta = -(itemTop - (scrollPoint.y + viewHeight));
1531 } else { // No need to scroll.
1535 [self performOneScroll:delta];
1538 // All changes to selectedness of buttons (aka fake menu items) ends up
1539 // calling this method to actually flip the state of items.
1540 // Needs to handle -1 as the invalid index (when nothing is selected) and
1541 // greater than range values too.
1542 - (void)setStateOfButtonByIndex:(int)index
1544 if (index >= 0 && index < [self buttonCount])
1545 [[buttons_ objectAtIndex:index] highlight:state];
1548 // Selects the required button and deselects the previously selected one.
1549 // An index of -1 means no selection.
1550 - (void)setSelectedButtonByIndex:(int)index {
1551 if (index == selectedIndex_)
1554 [self setStateOfButtonByIndex:selectedIndex_ state:NO];
1555 [self setStateOfButtonByIndex:index state:YES];
1556 selectedIndex_ = index;
1558 [self showSelectedButton];
1561 - (void)clearInputText {
1562 [typedPrefix_ release];
1566 // Find the earliest item in the folder which has the target prefix.
1567 // Returns nil if there is no prefix or there are no matches.
1568 // These are in no particular order, and not particularly numerous, so linear
1569 // search should be OK.
1570 // -1 means no match.
1571 - (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix {
1572 if ([prefix length] == 0) // Also handles nil.
1574 int maxButtons = [buttons_ count];
1575 NSString* lowercasePrefix = [prefix lowercaseString];
1576 for (int i = 0 ; i < maxButtons ; ++i) {
1577 BookmarkButton* button = [buttons_ objectAtIndex:i];
1578 if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix])
1584 - (void)setSelectedButtonByPrefix:(NSString*)prefix {
1585 [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]];
1588 - (void)selectPrevious {
1590 if (selectedIndex_ == 0)
1592 if (selectedIndex_ < 0)
1593 newIndex = [self buttonCount] -1;
1595 newIndex = std::max(selectedIndex_ - 1, 0);
1596 [self setSelectedButtonByIndex:newIndex];
1599 - (void)selectNext {
1600 if (selectedIndex_ + 1 < [self buttonCount])
1601 [self setSelectedButtonByIndex:selectedIndex_ + 1];
1604 - (BOOL)handleInputText:(NSString*)newText {
1605 const unichar kUnicodeEscape = 0x001B;
1606 const unichar kUnicodeSpace = 0x0020;
1608 // Event goes to the deepest nested open submenu.
1609 if (folderController_)
1610 return [folderController_ handleInputText:newText];
1612 // Look for arrow keys or other function keys.
1613 if ([newText length] == 1) {
1614 // Get the 16-bit unicode char.
1615 unichar theChar = [newText characterAtIndex:0];
1618 // Keys that trigger opening of the selection.
1619 case kUnicodeSpace: // Space.
1620 case NSNewlineCharacter:
1621 case NSCarriageReturnCharacter:
1622 case NSEnterCharacter:
1623 if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) {
1624 [barController_ openBookmark:[buttons_ objectAtIndex:selectedIndex_]];
1625 return NO; // NO because the selection-handling code will close later.
1627 return YES; // Triggering with no selection closes the menu.
1629 // Keys that cancel and close the menu.
1630 case kUnicodeEscape:
1631 case NSDeleteCharacter:
1632 case NSBackspaceCharacter:
1633 [self clearInputText];
1635 // Keys that change selection directionally.
1636 case NSUpArrowFunctionKey:
1637 [self clearInputText];
1638 [self selectPrevious];
1640 case NSDownArrowFunctionKey:
1641 [self clearInputText];
1644 // Keys that open and close submenus.
1645 case NSRightArrowFunctionKey: {
1646 BookmarkButton* btn = [self buttonAtIndex:selectedIndex_];
1647 if (btn && [btn isFolder]) {
1648 [self openBookmarkFolderFromButtonAndCloseOldOne:btn];
1649 [folderController_ selectNext];
1651 [self clearInputText];
1654 case NSLeftArrowFunctionKey:
1655 [self clearInputText];
1656 [parentController_ closeBookmarkFolder:self];
1659 // Check for other keys that should close the menu.
1661 if (theChar > NSUpArrowFunctionKey &&
1662 theChar <= NSModeSwitchFunctionKey) {
1663 [self clearInputText];
1671 // It is a char or string worth adding to the type-select buffer.
1672 NSString* newString = (!typedPrefix_) ?
1673 newText : [typedPrefix_ stringByAppendingString:newText];
1674 [typedPrefix_ release];
1675 typedPrefix_ = [newString retain];
1676 [self setSelectedButtonByPrefix:typedPrefix_];
1680 // Return the y position for a drop indicator.
1682 // TODO(jrg): again we have code dup, sort of, with
1683 // bookmark_bar_controller.mm, but the axis is changed.
1684 // http://crbug.com/35966
1685 - (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
1687 int destIndex = [self indexForDragToPoint:point];
1688 int numButtons = static_cast<int>([buttons_ count]);
1690 // If it's a drop strictly between existing buttons or at the very beginning
1691 if (destIndex >= 0 && destIndex < numButtons) {
1692 // ... put the indicator right between the buttons.
1693 BookmarkButton* button =
1694 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)];
1696 NSRect buttonFrame = [button frame];
1697 y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding;
1699 // If it's a drop at the end (past the last button, if there are any) ...
1700 } else if (destIndex == numButtons) {
1701 // and if it's past the last button ...
1702 if (numButtons > 0) {
1703 // ... find the last button, and put the indicator below it.
1704 BookmarkButton* button =
1705 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
1707 NSRect buttonFrame = [button frame];
1708 y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding;
1718 - (ThemeService*)themeService {
1719 return [parentController_ themeService];
1722 - (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
1726 - (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
1730 - (BookmarkBarFolderController*)folderController {
1731 return folderController_;
1734 - (void)faviconLoadedForNode:(const BookmarkNode*)node {
1735 for (BookmarkButton* button in buttons_.get()) {
1736 if ([button bookmarkNode] == node) {
1737 [button setImage:[barController_ faviconForNode:node]];
1738 [button setNeedsDisplay:YES];
1743 // Node was not in this menu, try submenu.
1744 if (folderController_)
1745 [folderController_ faviconLoadedForNode:node];
1748 // Add a new folder controller as triggered by the given folder button.
1749 - (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
1750 if (folderController_)
1751 [self closeBookmarkFolder:self];
1753 // Folder controller, like many window controllers, owns itself.
1755 [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
1756 parentController:self
1757 barController:barController_
1759 [folderController_ showWindow:self];
1762 - (void)openAll:(const BookmarkNode*)node
1763 disposition:(WindowOpenDisposition)disposition {
1764 [barController_ openAll:node disposition:disposition];
1767 - (void)addButtonForNode:(const BookmarkNode*)node
1768 atIndex:(NSInteger)buttonIndex {
1769 // Propose the frame for the new button. By default, this will be set to the
1770 // topmost button's frame (and there will always be one) offset upward in
1771 // anticipation of insertion.
1772 NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
1773 newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1774 // When adding a button to an empty folder we must remove the 'empty'
1775 // placeholder button. This can be detected by checking for a parent
1776 // child count of 1.
1777 const BookmarkNode* parentNode = node->parent();
1778 if (parentNode->child_count() == 1) {
1779 BookmarkButton* emptyButton = [buttons_ lastObject];
1780 newButtonFrame = [emptyButton frame];
1781 [emptyButton setDelegate:nil];
1782 [emptyButton removeFromSuperview];
1783 [buttons_ removeLastObject];
1786 if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
1787 buttonIndex = [buttons_ count];
1789 // Offset upward by one button height all buttons above insertion location.
1790 BookmarkButton* button = nil; // Remember so it can be de-highlighted.
1791 for (NSInteger i = 0; i < buttonIndex; ++i) {
1792 button = [buttons_ objectAtIndex:i];
1793 // Remember this location in case it's the last button being moved
1794 // which is where the new button will be located.
1795 newButtonFrame = [button frame];
1796 NSRect buttonFrame = [button frame];
1797 buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1798 [button setFrame:buttonFrame];
1800 [[button cell] mouseExited:nil]; // De-highlight.
1801 BookmarkButton* newButton = [self makeButtonForNode:node
1802 frame:newButtonFrame];
1803 [buttons_ insertObject:newButton atIndex:buttonIndex];
1804 [folderView_ addSubview:newButton];
1806 // Close any child folder(s) which may still be open.
1807 [self closeBookmarkFolder:self];
1809 [self adjustWindowForButtonCount:[buttons_ count]];
1812 // More code which essentially duplicates that of BookmarkBarController.
1813 // TODO(mrossetti,jrg): http://crbug.com/35966
1814 - (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
1815 DCHECK([urls count] == [titles count]);
1816 BOOL nodesWereAdded = NO;
1817 // Figure out where these new bookmarks nodes are to be added.
1818 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
1819 BookmarkModel* bookmarkModel = [self bookmarkModel];
1820 const BookmarkNode* destParent = NULL;
1822 if ([button isFolder]) {
1823 destParent = [button bookmarkNode];
1824 // Drop it at the end.
1825 destIndex = [button bookmarkNode]->child_count();
1827 // Else we're dropping somewhere in the folder, so find the right spot.
1828 destParent = [parentButton_ bookmarkNode];
1829 destIndex = [self indexForDragToPoint:point];
1830 // Be careful if the number of buttons != number of nodes.
1831 destIndex += [[parentButton_ cell] startingChildIndex];
1834 ChromeBookmarkClient* client =
1835 ChromeBookmarkClientFactory::GetForProfile(profile_);
1836 if (!client->CanBeEditedByUser(destParent))
1839 // Create and add the new bookmark nodes.
1840 size_t urlCount = [urls count];
1841 for (size_t i = 0; i < urlCount; ++i) {
1843 const char* string = [[urls objectAtIndex:i] UTF8String];
1845 gurl = GURL(string);
1846 // We only expect to receive valid URLs.
1847 DCHECK(gurl.is_valid());
1848 if (gurl.is_valid()) {
1849 bookmarkModel->AddURL(destParent,
1851 base::SysNSStringToUTF16([titles objectAtIndex:i]),
1853 nodesWereAdded = YES;
1856 return nodesWereAdded;
1859 - (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
1860 if (fromIndex != toIndex) {
1862 toIndex = [buttons_ count];
1863 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
1864 if (movedButton == buttonThatMouseIsIn_)
1865 buttonThatMouseIsIn_ = nil;
1866 [buttons_ removeObjectAtIndex:fromIndex];
1867 NSRect movedFrame = [movedButton frame];
1868 NSPoint toOrigin = movedFrame.origin;
1869 [movedButton setHidden:YES];
1870 if (fromIndex < toIndex) {
1871 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
1872 toOrigin = [targetButton frame].origin;
1873 for (NSInteger i = fromIndex; i < toIndex; ++i) {
1874 BookmarkButton* button = [buttons_ objectAtIndex:i];
1875 NSRect frame = [button frame];
1876 frame.origin.y += bookmarks::kBookmarkFolderButtonHeight;
1877 [button setFrameOrigin:frame.origin];
1880 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
1881 toOrigin = [targetButton frame].origin;
1882 for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
1883 BookmarkButton* button = [buttons_ objectAtIndex:i];
1884 NSRect buttonFrame = [button frame];
1885 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1886 [button setFrameOrigin:buttonFrame.origin];
1889 [buttons_ insertObject:movedButton atIndex:toIndex];
1890 [movedButton setFrameOrigin:toOrigin];
1891 [movedButton setHidden:NO];
1895 // TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
1896 - (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
1897 // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
1898 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
1899 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
1901 // If this button has an open sub-folder, close it.
1902 if ([folderController_ parentButton] == oldButton)
1903 [self closeBookmarkFolder:self];
1905 // If a hover-open is pending, cancel it.
1906 if (oldButton == buttonThatMouseIsIn_) {
1907 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1908 buttonThatMouseIsIn_ = nil;
1911 // Deleting a button causes rearrangement that enables us to lose a
1912 // mouse-exited event. This problem doesn't appear to exist with
1913 // other keep-menu-open options (e.g. add folder). Since the
1914 // showsBorderOnlyWhileMouseInside uses a tracking area, simple
1915 // tricks (e.g. sending an extra mouseExited: to the button) don't
1917 // http://crbug.com/54324
1918 for (NSButton* button in buttons_.get()) {
1919 if ([button showsBorderOnlyWhileMouseInside]) {
1920 [button setShowsBorderOnlyWhileMouseInside:NO];
1921 [button setShowsBorderOnlyWhileMouseInside:YES];
1925 [oldButton setDelegate:nil];
1926 [oldButton removeFromSuperview];
1927 [buttons_ removeObjectAtIndex:buttonIndex];
1928 for (NSInteger i = 0; i < buttonIndex; ++i) {
1929 BookmarkButton* button = [buttons_ objectAtIndex:i];
1930 NSRect buttonFrame = [button frame];
1931 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight;
1932 [button setFrame:buttonFrame];
1934 // Search for and adjust submenus, if necessary.
1935 NSInteger buttonCount = [buttons_ count];
1937 BookmarkButton* subButton = [folderController_ parentButton];
1938 for (NSButton* aButton in buttons_.get()) {
1939 // If this button is showing its menu then we need to move the menu, too.
1940 if (aButton == subButton)
1942 offsetFolderMenuWindow:NSMakeSize(0.0, chrome::kBookmarkBarHeight)];
1944 } else if (parentButton_ != [barController_ otherBookmarksButton]) {
1945 // If all nodes have been removed from this folder then add in the
1946 // 'empty' placeholder button except for "Other bookmarks" folder
1947 // as we are going to hide it.
1948 NSRect buttonFrame =
1949 GetFirstButtonFrameForHeight([self menuHeightForButtonCount:1]);
1950 BookmarkButton* button = [self makeButtonForNode:nil
1952 [buttons_ addObject:button];
1953 [folderView_ addSubview:button];
1957 // buttonCount will be 0 if "Other bookmarks" folder is empty, so close
1958 // the folder before hiding it.
1959 if (buttonCount == 0)
1960 [barController_ closeBookmarkFolder:nil];
1961 else if (buttonCount > 0)
1962 [self adjustWindowForButtonCount:buttonCount];
1964 if (animate && !ignoreAnimations_)
1965 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
1966 NSZeroSize, nil, nil, nil);
1969 - (id<BookmarkButtonControllerProtocol>)controllerForNode:
1970 (const BookmarkNode*)node {
1971 // See if we are holding this node, otherwise see if it is in our
1972 // hierarchy of visible folder menus.
1973 if ([parentButton_ bookmarkNode] == node)
1975 return [folderController_ controllerForNode:node];
1978 #pragma mark TestingAPI Only
1980 - (BOOL)canScrollUp {
1981 return ![scrollUpArrowView_ isHidden];
1984 - (BOOL)canScrollDown {
1985 return ![scrollDownArrowView_ isHidden];
1988 - (CGFloat)verticalScrollArrowHeight {
1989 return verticalScrollArrowHeight_;
1992 - (NSView*)visibleView {
1993 return visibleView_;
1996 - (NSScrollView*)scrollView {
2000 - (NSView*)folderView {
2004 - (void)setIgnoreAnimations:(BOOL)ignore {
2005 ignoreAnimations_ = ignore;
2008 - (BookmarkButton*)buttonThatMouseIsIn {
2009 return buttonThatMouseIsIn_;
2012 @end // BookmarkBarFolderController