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