bookmark_bar_folder_controller.mm revision 5f1c94371a64b3196d4be9466099bb892df9b88e
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" 6 7#include "base/mac/bundle_locations.h" 8#include "base/mac/mac_util.h" 9#include "base/strings/sys_string_conversions.h" 10#import "chrome/browser/bookmarks/bookmark_model_factory.h" 11#import "chrome/browser/bookmarks/chrome_bookmark_client.h" 12#import "chrome/browser/bookmarks/chrome_bookmark_client_factory.h" 13#import "chrome/browser/profiles/profile.h" 14#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" 15#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" 16#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h" 17#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h" 18#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h" 19#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" 20#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" 21#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" 22#import "chrome/browser/ui/cocoa/browser_window_controller.h" 23#include "components/bookmarks/browser/bookmark_model.h" 24#include "components/bookmarks/browser/bookmark_node_data.h" 25#include "ui/base/theme_provider.h" 26 27using bookmarks::BookmarkNodeData; 28using bookmarks::kBookmarkBarMenuCornerRadius; 29 30namespace { 31 32// Frequency of the scrolling timer in seconds. 33const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1; 34 35// Amount to scroll by per timer fire. We scroll rather slowly; to 36// accomodate we do several at a time. 37const CGFloat kBookmarkBarFolderScrollAmount = 38 3 * bookmarks::kBookmarkFolderButtonHeight; 39 40// Amount to scroll for each scroll wheel roll. 41const CGFloat kBookmarkBarFolderScrollWheelAmount = 42 1 * bookmarks::kBookmarkFolderButtonHeight; 43 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. 48struct 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 51 // scroll view). 52 CGFloat windowLeft; 53 NSSize windowSize; 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. 57 CGFloat scrollDelta; 58 NSRect windowFrame; 59 NSRect visibleFrame; 60 NSRect scrollerFrame; 61 NSPoint scrollPoint; 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. 65 BOOL couldScrollUp; 66 BOOL canScrollUp; 67 BOOL couldScrollDown; 68 BOOL canScrollDown; 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 71 // flickering. 72 BOOL preScroll; 73 74 // Intermediate metrics used in determining window vertical layout changes. 75 CGFloat deltaWindowHeight; 76 CGFloat deltaWindowY; 77 CGFloat deltaVisibleHeight; 78 CGFloat deltaVisibleY; 79 CGFloat deltaScrollerHeight; 80 CGFloat deltaScrollerY; 81 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). 85 86 // Bottom of the screen's available area (excluding dock height and padding). 87 CGFloat minimumY; 88 // Bottom of the screen. 89 CGFloat screenBottomY; 90 CGFloat oldWindowY; 91 CGFloat folderY; 92 CGFloat folderTop; 93 94 LayoutMetrics(CGFloat windowLeft, NSSize windowSize, CGFloat scrollDelta) : 95 windowLeft(windowLeft), 96 windowSize(windowSize), 97 scrollDelta(scrollDelta), 98 couldScrollUp(NO), 99 canScrollUp(NO), 100 couldScrollDown(NO), 101 canScrollDown(NO), 102 preScroll(NO), 103 deltaWindowHeight(0.0), 104 deltaWindowY(0.0), 105 deltaVisibleHeight(0.0), 106 deltaVisibleY(0.0), 107 deltaScrollerHeight(0.0), 108 deltaScrollerY(0.0), 109 minimumY(0.0), 110 screenBottomY(0.0), 111 oldWindowY(0.0), 112 folderY(0.0), 113 folderTop(0.0) {} 114}; 115 116NSRect GetFirstButtonFrameForHeight(CGFloat height) { 117 CGFloat y = height - bookmarks::kBookmarkFolderButtonHeight - 118 bookmarks::kBookmarkVerticalPadding; 119 return NSMakeRect(0, y, bookmarks::kDefaultBookmarkWidth, 120 bookmarks::kBookmarkFolderButtonHeight); 121} 122 123} // namespace 124 125 126// Required to set the right tracking bounds for our fake menus. 127@interface NSView(Private) 128- (void)_updateTrackingAreas; 129@end 130 131@interface BookmarkBarFolderController(Private) 132- (void)configureWindow; 133- (void)addOrUpdateScrollTracking; 134- (void)removeScrollTracking; 135- (void)endScroll; 136- (void)addScrollTimerWithDelta:(CGFloat)delta; 137 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; 141 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; 146 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; 150 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. 161// 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; 169 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| 173// structure. 174- (void)gatherMetrics:(LayoutMetrics*)layoutMetrics; 175 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; 183 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; 188 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; 193 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; 197 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; 205 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; 214 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 221 to:(NSPoint)point 222 copy:(BOOL)copy; 223 224@end 225 226@interface BookmarkButton (BookmarkBarFolderMenuHighlighting) 227 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; 231 232@end 233 234@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting) 235 236- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn { 237 [self setShowsBorderOnlyWhileMouseInside:!forceOn]; 238 [self setNeedsDisplay]; 239} 240 241@end 242 243@implementation BookmarkBarFolderController 244 245@synthesize subFolderGrowthToRight = subFolderGrowthToRight_; 246 247- (id)initWithParentButton:(BookmarkButton*)button 248 parentController:(BookmarkBarFolderController*)parentController 249 barController:(BookmarkBarController*)barController 250 profile:(Profile*)profile { 251 NSString* nibPath = 252 [base::mac::FrameworkBundle() pathForResource:@"BookmarkBarFolderWindow" 253 ofType:@"nib"]; 254 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { 255 parentButton_.reset([button retain]); 256 selectedIndex_ = -1; 257 258 profile_ = profile; 259 260 // We want the button to remain bordered as part of the menu path. 261 [button forceButtonBorderToStayOnAlways:YES]; 262 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)) { 276 screen_ = screen; 277 break; 278 } 279 } 280 if (!screen_) { 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]; 288 } 289 290 parentController_.reset([parentController retain]); 291 if (!parentController_) 292 [self setSubFolderGrowthToRight:YES]; 293 else 294 [self setSubFolderGrowthToRight:[parentController 295 subFolderGrowthToRight]]; 296 barController_ = barController; // WEAK 297 buttons_.reset([[NSMutableArray alloc] init]); 298 folderTarget_.reset( 299 [[BookmarkFolderTarget alloc] initWithController:self profile:profile]); 300 [self configureWindow]; 301 hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]); 302 } 303 return self; 304} 305 306- (void)dealloc { 307 [self clearInputText]; 308 309 // The button is no longer part of the menu path. 310 [parentButton_ forceButtonBorderToStayOnAlways:NO]; 311 [parentButton_ setNeedsDisplay]; 312 313 [self removeScrollTracking]; 314 [self endScroll]; 315 [hoverState_ draggingExited]; 316 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]; 322 } 323 324 // Note: we don't need to 325 // [NSObject cancelPreviousPerformRequestsWithTarget:self]; 326 // Because all of our performSelector: calls use withDelay: which 327 // retains us. 328 [super dealloc]; 329} 330 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]); 336} 337 338// Overriden from NSWindowController to call childFolderWillShow: before showing 339// the window. 340- (void)showWindow:(id)sender { 341 [barController_ childFolderWillShow:self]; 342 [super showWindow:sender]; 343} 344 345- (int)buttonCount { 346 return [[self buttons] count]; 347} 348 349- (BookmarkButton*)parentButton { 350 return parentButton_.get(); 351} 352 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]; 360} 361 362- (void)reconfigureMenu { 363 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 364 for (BookmarkButton* button in buttons_.get()) { 365 [button setDelegate:nil]; 366 [button removeFromSuperview]; 367 } 368 [buttons_ removeAllObjects]; 369 [self configureWindow]; 370} 371 372#pragma mark Private Methods 373 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 380 text:nil 381 image:image 382 menuController:menuController]; 383 [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; 384 return cell; 385} 386 387// Redirect to our logic shared with BookmarkBarController. 388- (IBAction)openBookmarkFolderFromButton:(id)sender { 389 [folderTarget_ openBookmarkFolderFromButton:sender]; 390} 391 392// Create a bookmark button for the given node using frame. 393// 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. 398// 399// TODO(jrg): combine with addNodesToButtonList: code from 400// bookmark_bar_controller.mm, and generalize that to use both x and y 401// offsets. 402// http://crbug.com/35966 403- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node 404 frame:(NSRect)frame { 405 BookmarkButtonCell* cell = [self cellForBookmarkNode:node]; 406 DCHECK(cell); 407 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]; 415 } 416 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. 423 frame.size.width = 424 std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth, 425 std::max(frame.size.width, desired)), 426 bookmarks::kBookmarkMenuButtonMaximumWidth); 427 428 BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame] 429 autorelease]; 430 DCHECK(button); 431 432 [button setCell:cell]; 433 [button setDelegate:self]; 434 if (node) { 435 if (node->is_folder()) { 436 [button setTarget:self]; 437 [button setAction:@selector(openBookmarkFolderFromButton:)]; 438 } else { 439 // Make the button do something. 440 [button setTarget:barController_]; 441 [button setAction:@selector(openBookmark:)]; 442 // Add a tooltip. 443 [button setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]]; 444 [button setAcceptsTrackIn:YES]; 445 } 446 } else { 447 [button setEnabled:NO]; 448 [button setBordered:NO]; 449 } 450 return button; 451} 452 453- (id)folderTarget { 454 return folderTarget_.get(); 455} 456 457 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 464// default. 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. 470 CGFloat x; 471 int tries = 0; 472 while (tries < 2) { 473 // Try to grow right. 474 if ([self subFolderGrowthToRight]) { 475 tries++; 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]; 483 } else { 484 return x; 485 } 486 } 487 // Try to grow left. 488 if (![self subFolderGrowthToRight]) { 489 tries++; 490 x = NSMinX([[parentButton_ window] frame]) + 491 bookmarks::kBookmarkMenuOverlap - 492 windowWidth; 493 // If off the screen, switch direction. 494 if (x < NSMinX([screen_ visibleFrame])) { 495 [self setSubFolderGrowthToRight:YES]; 496 } else { 497 return x; 498 } 499 } 500 } 501 // Unhappy; do the best we can. 502 return NSMaxX([screen_ visibleFrame]) - windowWidth; 503} 504 505 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)); 537 } 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)); 548 } 549 } else { 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; 560 } 561 return newWindowTopLeft; 562} 563 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]; 568} 569 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; 575} 576 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. 588 589 LayoutMetrics layoutMetrics(windowLeft, windowSize, scrollDelta); 590 [self gatherMetrics:&layoutMetrics]; 591 [self adjustMetrics:&layoutMetrics]; 592 [self applyMetrics:&layoutMetrics]; 593} 594 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]; 605 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; 612 613 metrics.minimumY = NSMinY([screen_ visibleFrame]) + 614 bookmarks::kScrollWindowVerticalMargin; 615 metrics.screenBottomY = NSMinY([screen_ frame]); 616 metrics.oldWindowY = NSMinY(metrics.windowFrame); 617 metrics.folderY = 618 metrics.scrollerFrame.origin.y + metrics.visibleFrame.origin.y + 619 metrics.oldWindowY - metrics.scrollPoint.y; 620 metrics.folderTop = metrics.folderY + NSHeight([folderView_ frame]); 621} 622 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; 629 CGFloat maximumY = 630 NSMaxY([screen_ visibleFrame]) - bookmarks::kScrollWindowVerticalMargin; 631 metrics.canScrollDown = metrics.folderTop > maximumY; 632 633 // Accommodate changes in the bottom of the menu. 634 [self adjustMetricsForMenuBottomChanges:layoutMetrics]; 635 636 // Accommodate changes in the top of the menu. 637 [self adjustMetricsForMenuTopChanges:layoutMetrics]; 638 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; 649} 650 651- (void)adjustMetricsForMenuBottomChanges:(LayoutMetrics*)layoutMetrics { 652 LayoutMetrics& metrics(*layoutMetrics); 653 if (metrics.canScrollUp) { 654 if (!metrics.couldScrollUp) { 655 // Couldn't -> Can 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; 669 } 670 } else if (!metrics.canScrollDown && metrics.windowSize.height > 0.0) { 671 metrics.scrollPoint.y += metrics.windowSize.height; 672 } 673 } else { 674 if (metrics.couldScrollUp) { 675 // Could -> Can't 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; 684 } else { 685 // Couldn't -> Can't 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 688 // the same. 689 metrics.deltaWindowY = NSMaxY(metrics.windowFrame) - metrics.folderTop; 690 metrics.deltaWindowHeight = -metrics.deltaWindowY; 691 } 692 } 693} 694 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; 701 } 702 } else { 703 if (metrics.canScrollDown) { 704 // Couldn't -> Can 705 const CGFloat maximumY = NSMaxY([screen_ visibleFrame]); 706 metrics.deltaWindowHeight += (maximumY - NSMaxY(metrics.windowFrame)); 707 metrics.deltaVisibleHeight -= bookmarks::kScrollWindowVerticalMargin; 708 metrics.deltaScrollerHeight -= verticalScrollArrowHeight_; 709 } else { 710 // Could -> Can't 711 metrics.deltaWindowHeight -= bookmarks::kScrollWindowVerticalMargin; 712 metrics.deltaVisibleHeight += bookmarks::kScrollWindowVerticalMargin; 713 metrics.deltaScrollerHeight += verticalScrollArrowHeight_; 714 } 715 } 716} 717 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]; 725 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]; 734 735 [[self window] setFrame:metrics.windowFrame display:YES]; 736 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]; 741 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]; 750 751 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]; 757 else 758 [self removeScrollTracking]; 759 } 760} 761 762- (void)adjustWindowForButtonCount:(NSUInteger)buttonCount { 763 NSRect folderFrame = [folderView_ frame]; 764 CGFloat newMenuHeight = 765 (CGFloat)[self menuHeightForButtonCount:[buttons_ count]]; 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]; 773 } 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]; 782} 783 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]; 789 DCHECK(node); 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); 794 795 // Prelim height of the window. We'll trim later as needed. 796 int height = [self menuHeightForButtonCount:buttons]; 797 // We'll need this soon... 798 [self window]; 799 800 // TODO(jrg): combine with frame code in bookmark_bar_controller.mm 801 // http://crbug.com/35966 802 NSRect buttonsOuterFrame = GetFirstButtonFrameForHeight(height); 803 804 // TODO(jrg): combine with addNodesToButtonList: code from 805 // bookmark_bar_controller.mm (but use y offset) 806 // http://crbug.com/35966 807 if (node->empty()) { 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]; 813 } else { 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; 821 } 822 } 823 [self layOutWindowWithHeight:height]; 824} 825 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 832 height:height]; 833 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 + 839 height; 840 newWindowTopLeft.y = MAX(newWindowTopLeft.y, minimumY); 841 } 842 843 NSWindow* window = [self window]; 844 NSRect windowFrame = NSMakeRect(newWindowTopLeft.x, 845 newWindowTopLeft.y - height, 846 windowWidth, height); 847 [window setFrame:windowFrame display:NO]; 848 849 NSRect folderFrame = NSMakeRect(0, 0, windowWidth, height); 850 [folderView_ setFrame:folderFrame]; 851 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]; 857 858 NSSize newSize = NSMakeSize(windowWidth, 0.0); 859 [self adjustWindowLeft:newWindowTopLeft.x size:newSize scrollingBy:0.0]; 860 [self configureWindowLevel]; 861 862 [window display]; 863} 864 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]; 883 } 884 return width; 885} 886 887// Start a "scroll up" timer. 888- (void)beginScrollWindowUp { 889 [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount]; 890} 891 892// Start a "scroll down" timer. 893- (void)beginScrollWindowDown { 894 [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount]; 895} 896 897// End a scrolling timer. Can be called excessively with no harm. 898- (void)endScroll { 899 if (scrollTimer_) { 900 [scrollTimer_ invalidate]; 901 scrollTimer_ = nil; 902 verticalScrollDelta_ = 0; 903 } 904} 905 906- (int)indexOfButton:(BookmarkButton*)button { 907 if (button == nil) 908 return -1; 909 NSInteger index = [buttons_ indexOfObject:button]; 910 return (index == NSNotFound) ? -1 : index; 911} 912 913- (BookmarkButton*)buttonAtIndex:(int)which { 914 if (which < 0 || which >= [self buttonCount]) 915 return nil; 916 return [buttons_ objectAtIndex:which]; 917} 918 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]; 927 return YES; 928 } 929 return NO; 930} 931 932// Perform a single scroll of the specified amount. 933- (void)performOneScroll:(CGFloat)delta { 934 if (delta == 0.0) 935 return; 936 CGFloat finalDelta = [self determineFinalScrollDelta:delta]; 937 if (finalDelta == 0.0) 938 return; 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 949 size:newSize 950 scrollingBy:finalDelta]; 951 // We have now scrolled. 952 if (!maintainHoverSelection) 953 return; 954 // Is mouse still in the same hovered button? 955 if ([[buttonThatMouseIsIn_ cell] isMouseReallyInside]) 956 return; 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. 960 index--; 961 while (index >= 0) { 962 if ([self selectButtonIfHoveredAtIndex:index]) 963 return; 964 index--; 965 } 966 } else { // Scrolled down, so search forward for new hovered button. 967 index++; 968 int btnMax = [self buttonCount]; 969 while (index < btnMax) { 970 if ([self selectButtonIfHoveredAtIndex:index]) 971 return; 972 index++; 973 } 974 } 975} 976 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; 990 991 if (delta > 0.0) { 992 // Scrolling up. 993 CGFloat minimumY = NSMinY([screen_ visibleFrame]) + 994 bookmarks::kScrollWindowVerticalMargin; 995 CGFloat maxUpDelta = scrollY - offset + minimumY; 996 delta = MIN(delta, maxUpDelta); 997 } else { 998 // Scrolling down. 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); 1007 } 1008 } else { 1009 delta = 0.0; 1010 } 1011 return delta; 1012} 1013 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_]; 1019} 1020 1021 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) 1026 return; 1027 [self endScroll]; 1028 verticalScrollDelta_ = delta; 1029 scrollTimer_ = [NSTimer timerWithTimeInterval:kBookmarkBarFolderScrollInterval 1030 target:self 1031 selector:@selector(performScroll:) 1032 userInfo:nil 1033 repeats:YES]; 1034 1035 [[NSRunLoop mainRunLoop] addTimer:scrollTimer_ forMode:NSRunLoopCommonModes]; 1036} 1037 1038 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 1043// initializer). 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]]; 1049 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 1053 toView:nil]; 1054 testPoint = [[self window] convertBaseToScreen:testPoint]; 1055 CGFloat closeToTopOfScreen = testPoint.y; 1056 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]; 1067 } else { 1068 [self endScroll]; 1069 } 1070} 1071 1072- (void)mouseMoved:(NSEvent*)theEvent { 1073 [self mouseMovedOrDragged:theEvent]; 1074} 1075 1076- (void)mouseDragged:(NSEvent*)theEvent { 1077 [self mouseMovedOrDragged:theEvent]; 1078} 1079 1080- (void)mouseExited:(NSEvent*)theEvent { 1081 [self endScroll]; 1082} 1083 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 1096 ) 1097 owner:self 1098 userInfo:nil]); 1099 [view addTrackingArea:scrollTrackingArea_.get()]; 1100} 1101 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]; 1107 } 1108 scrollTrackingArea_.reset(); 1109} 1110 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) 1119 return; 1120 // If an old submenu exists, close it immediately. 1121 [self closeBookmarkFolder:sender]; 1122 1123 // Open a new one if meaningful. 1124 if ([sender isFolder]) 1125 [folderTarget_ openBookmarkFolderFromButton:sender]; 1126} 1127 1128- (NSArray*)buttons { 1129 return buttons_.get(); 1130} 1131 1132- (void)close { 1133 [folderController_ close]; 1134 [super close]; 1135} 1136 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]; 1142 } 1143} 1144 1145#pragma mark Drag & Drop 1146 1147// Find something like std::is_between<T>? I can't believe one doesn't exist. 1148// http://crbug.com/35966 1149static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { 1150 return ((value >= low) && (value <= high)); 1151} 1152 1153// Return the proposed drop target for a hover open button, or nil if none. 1154// 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. 1163 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]), 1167 localPoint.y, 1168 NSMaxY([button frame]))) { 1169 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), 1175 localPoint.y, 1176 NSMaxY(middleHalfOfButton))) { 1177 // It makes no sense to drop on a non-folder; there is no hover. 1178 if (![button isFolder]) 1179 return nil; 1180 // Got it! 1181 return button; 1182 } else { 1183 // Over a button but not over the middle half. 1184 return nil; 1185 } 1186 } 1187 } 1188 // Not hovering over a button. 1189 return nil; 1190} 1191 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) { 1213 break; 1214 } 1215 buttonToTheTopOfDraggedButton = button; 1216 } 1217 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). 1223 return 0; 1224 } 1225 if ([buttonToTheTopOfDraggedButton isEmpty]) { 1226 // There is a button but it's an empty placeholder. 1227 // Default to inserting on top of it. 1228 return 0; 1229 } 1230 const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton 1231 bookmarkNode]; 1232 DCHECK(beforeNode); 1233 // Be careful if the number of buttons != number of nodes. 1234 return ((beforeNode->parent()->GetIndexOf(beforeNode) + 1) - 1235 [[parentButton_ cell] startingChildIndex]); 1236} 1237 1238// TODO(jrg): Yet more code dup. 1239// http://crbug.com/35966 1240- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode 1241 to:(NSPoint)point 1242 copy:(BOOL)copy { 1243 DCHECK(sourceNode); 1244 1245 // Drop destination. 1246 const BookmarkNode* destParent = NULL; 1247 int destIndex = 0; 1248 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(); 1256 } else { 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]; 1262 } 1263 1264 ChromeBookmarkClient* client = 1265 ChromeBookmarkClientFactory::GetForProfile(profile_); 1266 if (!client->CanBeEditedByUser(destParent)) 1267 return NO; 1268 if (!client->CanBeEditedByUser(sourceNode)) 1269 copy = YES; 1270 1271 // Prevent cycles. 1272 BOOL wasCopiedOrMoved = NO; 1273 if (!destParent->HasAncestor(sourceNode)) { 1274 if (copy) 1275 [self bookmarkModel]->Copy(sourceNode, destParent, destIndex); 1276 else 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. 1281 } 1282 1283 return wasCopiedOrMoved; 1284} 1285 1286// TODO(maf): Implement live drag & drop animation using this hook. 1287- (void)setDropInsertionPos:(CGFloat)where { 1288} 1289 1290// TODO(maf): Implement live drag & drop animation using this hook. 1291- (void)clearDropInsertionPos { 1292} 1293 1294#pragma mark NSWindowDelegate Functions 1295 1296- (void)windowWillClose:(NSNotification*)notification { 1297 // Also done by the dealloc method, but also doing it here is quicker and 1298 // more reliable. 1299 [parentButton_ forceButtonBorderToStayOnAlways:NO]; 1300 1301 // If a "hover open" is pending when the bookmark bar folder is 1302 // closed, be sure it gets cancelled. 1303 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 1304 1305 [self endScroll]; // Just in case we were scrolling. 1306 [barController_ childFolderWillClose:self]; 1307 [self closeBookmarkFolder:self]; 1308 [self autorelease]; 1309} 1310 1311#pragma mark BookmarkButtonDelegate Protocol 1312 1313- (void)fillPasteboard:(NSPasteboard*)pboard 1314 forDragOfButton:(BookmarkButton*)button { 1315 [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; 1316 1317 // Close our folder menu and submenus since we know we're going to be dragged. 1318 [self closeBookmarkFolder:self]; 1319} 1320 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]; 1325 1326 buttonThatMouseIsIn_ = sender; 1327 [self setSelectedButtonByIndex:[self indexOfButton:sender]]; 1328 1329 // Cancel a previous hover if needed. 1330 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 1331 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) 1335 return; 1336 1337 [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:) 1338 withObject:sender 1339 afterDelay:bookmarks::kHoverOpenDelay 1340 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; 1341} 1342 1343// Called from the BookmarkButton 1344- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { 1345 if (buttonThatMouseIsIn_ == sender) 1346 buttonThatMouseIsIn_ = nil; 1347 [self setSelectedButtonByIndex:-1]; 1348 1349 // Stop any timer about opening a new hover-open folder. 1350 1351 // Since a performSelector:withDelay: on self retains self, it is 1352 // possible that a cancelPreviousPerformRequestsWithTarget: reduces 1353 // the refcount to 0, releasing us. That's a bad thing to do while 1354 // this object (or others it may own) is in the event chain. Thus 1355 // we have a retain/autorelease. 1356 [self retain]; 1357 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 1358 [self autorelease]; 1359} 1360 1361- (NSWindow*)browserWindow { 1362 return [barController_ browserWindow]; 1363} 1364 1365- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { 1366 return [barController_ canEditBookmarks] && 1367 [barController_ canEditBookmark:[button bookmarkNode]]; 1368} 1369 1370- (void)didDragBookmarkToTrash:(BookmarkButton*)button { 1371 [barController_ didDragBookmarkToTrash:button]; 1372} 1373 1374- (void)bookmarkDragDidEnd:(BookmarkButton*)button 1375 operation:(NSDragOperation)operation { 1376 [barController_ bookmarkDragDidEnd:button 1377 operation:operation]; 1378} 1379 1380 1381#pragma mark BookmarkButtonControllerProtocol 1382 1383// Recursively close all bookmark folders. 1384- (void)closeAllBookmarkFolders { 1385 // Closing the top level implicitly closes all children. 1386 [barController_ closeAllBookmarkFolders]; 1387} 1388 1389// Close our bookmark folder (a sub-controller) if we have one. 1390- (void)closeBookmarkFolder:(id)sender { 1391 if (folderController_) { 1392 // Make this menu key, so key status doesn't go back to the browser 1393 // window when the submenu closes. 1394 [[self window] makeKeyWindow]; 1395 [self setSubFolderGrowthToRight:YES]; 1396 [[folderController_ window] close]; 1397 folderController_ = nil; 1398 } 1399} 1400 1401- (BookmarkModel*)bookmarkModel { 1402 return [barController_ bookmarkModel]; 1403} 1404 1405- (BOOL)draggingAllowed:(id<NSDraggingInfo>)info { 1406 return [barController_ draggingAllowed:info]; 1407} 1408 1409// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966 1410// Most of the work (e.g. drop indicator) is taken care of in the 1411// folder_view. Here we handle hover open issues for subfolders. 1412// Caution: there are subtle differences between this one and 1413// bookmark_bar_controller.mm's version. 1414- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { 1415 NSPoint currentLocation = [info draggingLocation]; 1416 BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation]; 1417 1418 // Don't allow drops that would result in cycles. 1419 if (button) { 1420 NSData* data = [[info draggingPasteboard] 1421 dataForType:kBookmarkButtonDragType]; 1422 if (data && [info draggingSource]) { 1423 BookmarkButton* sourceButton = nil; 1424 [data getBytes:&sourceButton length:sizeof(sourceButton)]; 1425 const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; 1426 const BookmarkNode* destNode = [button bookmarkNode]; 1427 if (destNode->HasAncestor(sourceNode)) 1428 button = nil; 1429 } 1430 } 1431 // Delegate handling of dragging over a button to the |hoverState_| member. 1432 return [hoverState_ draggingEnteredButton:button]; 1433} 1434 1435- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info { 1436 return NSDragOperationMove; 1437} 1438 1439// Unlike bookmark_bar_controller, we need to keep track of dragging state. 1440// We also need to make sure we cancel the delayed hover close. 1441- (void)draggingExited:(id<NSDraggingInfo>)info { 1442 // NOT the same as a cancel --> we may have moved the mouse into the submenu. 1443 // Delegate handling of the hover button to the |hoverState_| member. 1444 [hoverState_ draggingExited]; 1445} 1446 1447- (BOOL)dragShouldLockBarVisibility { 1448 return [parentController_ dragShouldLockBarVisibility]; 1449} 1450 1451// TODO(jrg): ARGH more code dup. 1452// http://crbug.com/35966 1453- (BOOL)dragButton:(BookmarkButton*)sourceButton 1454 to:(NSPoint)point 1455 copy:(BOOL)copy { 1456 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); 1457 const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; 1458 return [self dragBookmark:sourceNode to:point copy:copy]; 1459} 1460 1461// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController. 1462// http://crbug.com/35966 1463- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { 1464 BOOL dragged = NO; 1465 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]); 1466 if (nodes.size()) { 1467 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); 1468 NSPoint dropPoint = [info draggingLocation]; 1469 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin(); 1470 it != nodes.end(); ++it) { 1471 const BookmarkNode* sourceNode = *it; 1472 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; 1473 } 1474 } 1475 return dragged; 1476} 1477 1478// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController. 1479// http://crbug.com/35966 1480- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { 1481 std::vector<const BookmarkNode*> dragDataNodes; 1482 BookmarkNodeData dragData; 1483 if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) { 1484 BookmarkModel* bookmarkModel = [self bookmarkModel]; 1485 std::vector<const BookmarkNode*> nodes( 1486 dragData.GetNodes(bookmarkModel, profile_->GetPath())); 1487 dragDataNodes.assign(nodes.begin(), nodes.end()); 1488 } 1489 return dragDataNodes; 1490} 1491 1492// Return YES if we should show the drop indicator, else NO. 1493// TODO(jrg): ARGH code dup! 1494// http://crbug.com/35966 1495- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { 1496 return ![self buttonForDroppingOnAtPoint:point]; 1497} 1498 1499// Button selection change code to support type to select and arrow key events. 1500#pragma mark Keyboard Support 1501 1502// Scroll the menu to show the selected button, if it's not already visible. 1503- (void)showSelectedButton { 1504 int bMaxIndex = [self buttonCount] - 1; // Max array index in button array. 1505 1506 // Is there a valid selected button? 1507 if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex) 1508 return; 1509 1510 // Is the menu scrollable anyway? 1511 if (![self canScrollUp] && ![self canScrollDown]) 1512 return; 1513 1514 // Now check to see if we need to scroll, which way, and how far. 1515 CGFloat delta = 0.0; 1516 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin; 1517 CGFloat itemBottom = (bMaxIndex - selectedIndex_) * 1518 bookmarks::kBookmarkFolderButtonHeight; 1519 CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight; 1520 CGFloat viewHeight = NSHeight([scrollView_ frame]); 1521 1522 if (scrollPoint.y > itemBottom) { // Need to scroll down. 1523 delta = scrollPoint.y - itemBottom; 1524 } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up. 1525 delta = -(itemTop - (scrollPoint.y + viewHeight)); 1526 } else { // No need to scroll. 1527 return; 1528 } 1529 1530 [self performOneScroll:delta]; 1531} 1532 1533// All changes to selectedness of buttons (aka fake menu items) ends up 1534// calling this method to actually flip the state of items. 1535// Needs to handle -1 as the invalid index (when nothing is selected) and 1536// greater than range values too. 1537- (void)setStateOfButtonByIndex:(int)index 1538 state:(bool)state { 1539 if (index >= 0 && index < [self buttonCount]) 1540 [[buttons_ objectAtIndex:index] highlight:state]; 1541} 1542 1543// Selects the required button and deselects the previously selected one. 1544// An index of -1 means no selection. 1545- (void)setSelectedButtonByIndex:(int)index { 1546 if (index == selectedIndex_) 1547 return; 1548 1549 [self setStateOfButtonByIndex:selectedIndex_ state:NO]; 1550 [self setStateOfButtonByIndex:index state:YES]; 1551 selectedIndex_ = index; 1552 1553 [self showSelectedButton]; 1554} 1555 1556- (void)clearInputText { 1557 [typedPrefix_ release]; 1558 typedPrefix_ = nil; 1559} 1560 1561// Find the earliest item in the folder which has the target prefix. 1562// Returns nil if there is no prefix or there are no matches. 1563// These are in no particular order, and not particularly numerous, so linear 1564// search should be OK. 1565// -1 means no match. 1566- (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix { 1567 if ([prefix length] == 0) // Also handles nil. 1568 return -1; 1569 int maxButtons = [buttons_ count]; 1570 NSString* lowercasePrefix = [prefix lowercaseString]; 1571 for (int i = 0 ; i < maxButtons ; ++i) { 1572 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1573 if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix]) 1574 return i; 1575 } 1576 return -1; 1577} 1578 1579- (void)setSelectedButtonByPrefix:(NSString*)prefix { 1580 [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]]; 1581} 1582 1583- (void)selectPrevious { 1584 int newIndex; 1585 if (selectedIndex_ == 0) 1586 return; 1587 if (selectedIndex_ < 0) 1588 newIndex = [self buttonCount] -1; 1589 else 1590 newIndex = std::max(selectedIndex_ - 1, 0); 1591 [self setSelectedButtonByIndex:newIndex]; 1592} 1593 1594- (void)selectNext { 1595 if (selectedIndex_ + 1 < [self buttonCount]) 1596 [self setSelectedButtonByIndex:selectedIndex_ + 1]; 1597} 1598 1599- (BOOL)handleInputText:(NSString*)newText { 1600 const unichar kUnicodeEscape = 0x001B; 1601 const unichar kUnicodeSpace = 0x0020; 1602 1603 // Event goes to the deepest nested open submenu. 1604 if (folderController_) 1605 return [folderController_ handleInputText:newText]; 1606 1607 // Look for arrow keys or other function keys. 1608 if ([newText length] == 1) { 1609 // Get the 16-bit unicode char. 1610 unichar theChar = [newText characterAtIndex:0]; 1611 switch (theChar) { 1612 1613 // Keys that trigger opening of the selection. 1614 case kUnicodeSpace: // Space. 1615 case NSNewlineCharacter: 1616 case NSCarriageReturnCharacter: 1617 case NSEnterCharacter: 1618 if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) { 1619 [barController_ openBookmark:[buttons_ objectAtIndex:selectedIndex_]]; 1620 return NO; // NO because the selection-handling code will close later. 1621 } else { 1622 return YES; // Triggering with no selection closes the menu. 1623 } 1624 // Keys that cancel and close the menu. 1625 case kUnicodeEscape: 1626 case NSDeleteCharacter: 1627 case NSBackspaceCharacter: 1628 [self clearInputText]; 1629 return YES; 1630 // Keys that change selection directionally. 1631 case NSUpArrowFunctionKey: 1632 [self clearInputText]; 1633 [self selectPrevious]; 1634 return NO; 1635 case NSDownArrowFunctionKey: 1636 [self clearInputText]; 1637 [self selectNext]; 1638 return NO; 1639 // Keys that open and close submenus. 1640 case NSRightArrowFunctionKey: { 1641 BookmarkButton* btn = [self buttonAtIndex:selectedIndex_]; 1642 if (btn && [btn isFolder]) { 1643 [self openBookmarkFolderFromButtonAndCloseOldOne:btn]; 1644 [folderController_ selectNext]; 1645 } 1646 [self clearInputText]; 1647 return NO; 1648 } 1649 case NSLeftArrowFunctionKey: 1650 [self clearInputText]; 1651 [parentController_ closeBookmarkFolder:self]; 1652 return NO; 1653 1654 // Check for other keys that should close the menu. 1655 default: { 1656 if (theChar > NSUpArrowFunctionKey && 1657 theChar <= NSModeSwitchFunctionKey) { 1658 [self clearInputText]; 1659 return YES; 1660 } 1661 break; 1662 } 1663 } 1664 } 1665 1666 // It is a char or string worth adding to the type-select buffer. 1667 NSString* newString = (!typedPrefix_) ? 1668 newText : [typedPrefix_ stringByAppendingString:newText]; 1669 [typedPrefix_ release]; 1670 typedPrefix_ = [newString retain]; 1671 [self setSelectedButtonByPrefix:typedPrefix_]; 1672 return NO; 1673} 1674 1675// Return the y position for a drop indicator. 1676// 1677// TODO(jrg): again we have code dup, sort of, with 1678// bookmark_bar_controller.mm, but the axis is changed. 1679// http://crbug.com/35966 1680- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { 1681 CGFloat y = 0; 1682 int destIndex = [self indexForDragToPoint:point]; 1683 int numButtons = static_cast<int>([buttons_ count]); 1684 1685 // If it's a drop strictly between existing buttons or at the very beginning 1686 if (destIndex >= 0 && destIndex < numButtons) { 1687 // ... put the indicator right between the buttons. 1688 BookmarkButton* button = 1689 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)]; 1690 DCHECK(button); 1691 NSRect buttonFrame = [button frame]; 1692 y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding; 1693 1694 // If it's a drop at the end (past the last button, if there are any) ... 1695 } else if (destIndex == numButtons) { 1696 // and if it's past the last button ... 1697 if (numButtons > 0) { 1698 // ... find the last button, and put the indicator below it. 1699 BookmarkButton* button = 1700 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; 1701 DCHECK(button); 1702 NSRect buttonFrame = [button frame]; 1703 y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding; 1704 1705 } 1706 } else { 1707 NOTREACHED(); 1708 } 1709 1710 return y; 1711} 1712 1713- (ThemeService*)themeService { 1714 return [parentController_ themeService]; 1715} 1716 1717- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { 1718 // Do nothing. 1719} 1720 1721- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { 1722 // Do nothing. 1723} 1724 1725- (BookmarkBarFolderController*)folderController { 1726 return folderController_; 1727} 1728 1729- (void)faviconLoadedForNode:(const BookmarkNode*)node { 1730 for (BookmarkButton* button in buttons_.get()) { 1731 if ([button bookmarkNode] == node) { 1732 [button setImage:[barController_ faviconForNode:node]]; 1733 [button setNeedsDisplay:YES]; 1734 return; 1735 } 1736 } 1737 1738 // Node was not in this menu, try submenu. 1739 if (folderController_) 1740 [folderController_ faviconLoadedForNode:node]; 1741} 1742 1743// Add a new folder controller as triggered by the given folder button. 1744- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { 1745 if (folderController_) 1746 [self closeBookmarkFolder:self]; 1747 1748 // Folder controller, like many window controllers, owns itself. 1749 folderController_ = 1750 [[BookmarkBarFolderController alloc] initWithParentButton:parentButton 1751 parentController:self 1752 barController:barController_ 1753 profile:profile_]; 1754 [folderController_ showWindow:self]; 1755} 1756 1757- (void)openAll:(const BookmarkNode*)node 1758 disposition:(WindowOpenDisposition)disposition { 1759 [barController_ openAll:node disposition:disposition]; 1760} 1761 1762- (void)addButtonForNode:(const BookmarkNode*)node 1763 atIndex:(NSInteger)buttonIndex { 1764 // Propose the frame for the new button. By default, this will be set to the 1765 // topmost button's frame (and there will always be one) offset upward in 1766 // anticipation of insertion. 1767 NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame]; 1768 newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight; 1769 // When adding a button to an empty folder we must remove the 'empty' 1770 // placeholder button. This can be detected by checking for a parent 1771 // child count of 1. 1772 const BookmarkNode* parentNode = node->parent(); 1773 if (parentNode->child_count() == 1) { 1774 BookmarkButton* emptyButton = [buttons_ lastObject]; 1775 newButtonFrame = [emptyButton frame]; 1776 [emptyButton setDelegate:nil]; 1777 [emptyButton removeFromSuperview]; 1778 [buttons_ removeLastObject]; 1779 } 1780 1781 if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count]) 1782 buttonIndex = [buttons_ count]; 1783 1784 // Offset upward by one button height all buttons above insertion location. 1785 BookmarkButton* button = nil; // Remember so it can be de-highlighted. 1786 for (NSInteger i = 0; i < buttonIndex; ++i) { 1787 button = [buttons_ objectAtIndex:i]; 1788 // Remember this location in case it's the last button being moved 1789 // which is where the new button will be located. 1790 newButtonFrame = [button frame]; 1791 NSRect buttonFrame = [button frame]; 1792 buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight; 1793 [button setFrame:buttonFrame]; 1794 } 1795 [[button cell] mouseExited:nil]; // De-highlight. 1796 BookmarkButton* newButton = [self makeButtonForNode:node 1797 frame:newButtonFrame]; 1798 [buttons_ insertObject:newButton atIndex:buttonIndex]; 1799 [folderView_ addSubview:newButton]; 1800 1801 // Close any child folder(s) which may still be open. 1802 [self closeBookmarkFolder:self]; 1803 1804 [self adjustWindowForButtonCount:[buttons_ count]]; 1805} 1806 1807// More code which essentially duplicates that of BookmarkBarController. 1808// TODO(mrossetti,jrg): http://crbug.com/35966 1809- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { 1810 DCHECK([urls count] == [titles count]); 1811 BOOL nodesWereAdded = NO; 1812 // Figure out where these new bookmarks nodes are to be added. 1813 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; 1814 BookmarkModel* bookmarkModel = [self bookmarkModel]; 1815 const BookmarkNode* destParent = NULL; 1816 int destIndex = 0; 1817 if ([button isFolder]) { 1818 destParent = [button bookmarkNode]; 1819 // Drop it at the end. 1820 destIndex = [button bookmarkNode]->child_count(); 1821 } else { 1822 // Else we're dropping somewhere in the folder, so find the right spot. 1823 destParent = [parentButton_ bookmarkNode]; 1824 destIndex = [self indexForDragToPoint:point]; 1825 // Be careful if the number of buttons != number of nodes. 1826 destIndex += [[parentButton_ cell] startingChildIndex]; 1827 } 1828 1829 ChromeBookmarkClient* client = 1830 ChromeBookmarkClientFactory::GetForProfile(profile_); 1831 if (!client->CanBeEditedByUser(destParent)) 1832 return NO; 1833 1834 // Create and add the new bookmark nodes. 1835 size_t urlCount = [urls count]; 1836 for (size_t i = 0; i < urlCount; ++i) { 1837 GURL gurl; 1838 const char* string = [[urls objectAtIndex:i] UTF8String]; 1839 if (string) 1840 gurl = GURL(string); 1841 // We only expect to receive valid URLs. 1842 DCHECK(gurl.is_valid()); 1843 if (gurl.is_valid()) { 1844 bookmarkModel->AddURL(destParent, 1845 destIndex++, 1846 base::SysNSStringToUTF16([titles objectAtIndex:i]), 1847 gurl); 1848 nodesWereAdded = YES; 1849 } 1850 } 1851 return nodesWereAdded; 1852} 1853 1854- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { 1855 if (fromIndex != toIndex) { 1856 if (toIndex == -1) 1857 toIndex = [buttons_ count]; 1858 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; 1859 if (movedButton == buttonThatMouseIsIn_) 1860 buttonThatMouseIsIn_ = nil; 1861 [buttons_ removeObjectAtIndex:fromIndex]; 1862 NSRect movedFrame = [movedButton frame]; 1863 NSPoint toOrigin = movedFrame.origin; 1864 [movedButton setHidden:YES]; 1865 if (fromIndex < toIndex) { 1866 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1]; 1867 toOrigin = [targetButton frame].origin; 1868 for (NSInteger i = fromIndex; i < toIndex; ++i) { 1869 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1870 NSRect frame = [button frame]; 1871 frame.origin.y += bookmarks::kBookmarkFolderButtonHeight; 1872 [button setFrameOrigin:frame.origin]; 1873 } 1874 } else { 1875 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex]; 1876 toOrigin = [targetButton frame].origin; 1877 for (NSInteger i = fromIndex - 1; i >= toIndex; --i) { 1878 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1879 NSRect buttonFrame = [button frame]; 1880 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight; 1881 [button setFrameOrigin:buttonFrame.origin]; 1882 } 1883 } 1884 [buttons_ insertObject:movedButton atIndex:toIndex]; 1885 [movedButton setFrameOrigin:toOrigin]; 1886 [movedButton setHidden:NO]; 1887 } 1888} 1889 1890// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966 1891- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { 1892 // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360 1893 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; 1894 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation]; 1895 1896 // If this button has an open sub-folder, close it. 1897 if ([folderController_ parentButton] == oldButton) 1898 [self closeBookmarkFolder:self]; 1899 1900 // If a hover-open is pending, cancel it. 1901 if (oldButton == buttonThatMouseIsIn_) { 1902 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 1903 buttonThatMouseIsIn_ = nil; 1904 } 1905 1906 // Deleting a button causes rearrangement that enables us to lose a 1907 // mouse-exited event. This problem doesn't appear to exist with 1908 // other keep-menu-open options (e.g. add folder). Since the 1909 // showsBorderOnlyWhileMouseInside uses a tracking area, simple 1910 // tricks (e.g. sending an extra mouseExited: to the button) don't 1911 // fix the problem. 1912 // http://crbug.com/54324 1913 for (NSButton* button in buttons_.get()) { 1914 if ([button showsBorderOnlyWhileMouseInside]) { 1915 [button setShowsBorderOnlyWhileMouseInside:NO]; 1916 [button setShowsBorderOnlyWhileMouseInside:YES]; 1917 } 1918 } 1919 1920 [oldButton setDelegate:nil]; 1921 [oldButton removeFromSuperview]; 1922 [buttons_ removeObjectAtIndex:buttonIndex]; 1923 for (NSInteger i = 0; i < buttonIndex; ++i) { 1924 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1925 NSRect buttonFrame = [button frame]; 1926 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight; 1927 [button setFrame:buttonFrame]; 1928 } 1929 // Search for and adjust submenus, if necessary. 1930 NSInteger buttonCount = [buttons_ count]; 1931 if (buttonCount) { 1932 BookmarkButton* subButton = [folderController_ parentButton]; 1933 for (NSButton* aButton in buttons_.get()) { 1934 // If this button is showing its menu then we need to move the menu, too. 1935 if (aButton == subButton) 1936 [folderController_ 1937 offsetFolderMenuWindow:NSMakeSize(0.0, chrome::kBookmarkBarHeight)]; 1938 } 1939 } else { 1940 // If all nodes have been removed from this folder then add in the 1941 // 'empty' placeholder button. 1942 NSRect buttonFrame = 1943 GetFirstButtonFrameForHeight([self menuHeightForButtonCount:1]); 1944 BookmarkButton* button = [self makeButtonForNode:nil 1945 frame:buttonFrame]; 1946 [buttons_ addObject:button]; 1947 [folderView_ addSubview:button]; 1948 buttonCount = 1; 1949 } 1950 1951 [self adjustWindowForButtonCount:buttonCount]; 1952 1953 if (animate && !ignoreAnimations_) 1954 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, 1955 NSZeroSize, nil, nil, nil); 1956} 1957 1958- (id<BookmarkButtonControllerProtocol>)controllerForNode: 1959 (const BookmarkNode*)node { 1960 // See if we are holding this node, otherwise see if it is in our 1961 // hierarchy of visible folder menus. 1962 if ([parentButton_ bookmarkNode] == node) 1963 return self; 1964 return [folderController_ controllerForNode:node]; 1965} 1966 1967#pragma mark TestingAPI Only 1968 1969- (BOOL)canScrollUp { 1970 return ![scrollUpArrowView_ isHidden]; 1971} 1972 1973- (BOOL)canScrollDown { 1974 return ![scrollDownArrowView_ isHidden]; 1975} 1976 1977- (CGFloat)verticalScrollArrowHeight { 1978 return verticalScrollArrowHeight_; 1979} 1980 1981- (NSView*)visibleView { 1982 return visibleView_; 1983} 1984 1985- (NSScrollView*)scrollView { 1986 return scrollView_; 1987} 1988 1989- (NSView*)folderView { 1990 return folderView_; 1991} 1992 1993- (void)setIgnoreAnimations:(BOOL)ignore { 1994 ignoreAnimations_ = ignore; 1995} 1996 1997- (BookmarkButton*)buttonThatMouseIsIn { 1998 return buttonThatMouseIsIn_; 1999} 2000 2001@end // BookmarkBarFolderController 2002