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:buttonCount]; 766 CGFloat deltaMenuHeight = newMenuHeight - NSHeight(folderFrame); 767 // If the height has changed then also change the origin, and adjust the 768 // scroll (if scrolling). 769 if ([self canScrollUp]) { 770 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin; 771 scrollPoint.y += deltaMenuHeight; 772 [[scrollView_ documentView] scrollPoint:scrollPoint]; 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 // If right click was done immediately on entering a button, then open the 1338 // folder without delay so that context menu appears over the folder menu. 1339 if ([event type] == NSRightMouseDown) 1340 [self openBookmarkFolderFromButtonAndCloseOldOne:sender]; 1341 else 1342 [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:) 1343 withObject:sender 1344 afterDelay:bookmarks::kHoverOpenDelay 1345 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; 1346} 1347 1348// Called from the BookmarkButton 1349- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { 1350 if (buttonThatMouseIsIn_ == sender) 1351 buttonThatMouseIsIn_ = nil; 1352 [self setSelectedButtonByIndex:-1]; 1353 1354 // Stop any timer about opening a new hover-open folder. 1355 1356 // Since a performSelector:withDelay: on self retains self, it is 1357 // possible that a cancelPreviousPerformRequestsWithTarget: reduces 1358 // the refcount to 0, releasing us. That's a bad thing to do while 1359 // this object (or others it may own) is in the event chain. Thus 1360 // we have a retain/autorelease. 1361 [self retain]; 1362 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 1363 [self autorelease]; 1364} 1365 1366- (NSWindow*)browserWindow { 1367 return [barController_ browserWindow]; 1368} 1369 1370- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { 1371 return [barController_ canEditBookmarks] && 1372 [barController_ canEditBookmark:[button bookmarkNode]]; 1373} 1374 1375- (void)didDragBookmarkToTrash:(BookmarkButton*)button { 1376 [barController_ didDragBookmarkToTrash:button]; 1377} 1378 1379- (void)bookmarkDragDidEnd:(BookmarkButton*)button 1380 operation:(NSDragOperation)operation { 1381 [barController_ bookmarkDragDidEnd:button 1382 operation:operation]; 1383} 1384 1385 1386#pragma mark BookmarkButtonControllerProtocol 1387 1388// Recursively close all bookmark folders. 1389- (void)closeAllBookmarkFolders { 1390 // Closing the top level implicitly closes all children. 1391 [barController_ closeAllBookmarkFolders]; 1392} 1393 1394// Close our bookmark folder (a sub-controller) if we have one. 1395- (void)closeBookmarkFolder:(id)sender { 1396 if (folderController_) { 1397 // Make this menu key, so key status doesn't go back to the browser 1398 // window when the submenu closes. 1399 [[self window] makeKeyWindow]; 1400 [self setSubFolderGrowthToRight:YES]; 1401 [[folderController_ window] close]; 1402 folderController_ = nil; 1403 } 1404} 1405 1406- (BookmarkModel*)bookmarkModel { 1407 return [barController_ bookmarkModel]; 1408} 1409 1410- (BOOL)draggingAllowed:(id<NSDraggingInfo>)info { 1411 return [barController_ draggingAllowed:info]; 1412} 1413 1414// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966 1415// Most of the work (e.g. drop indicator) is taken care of in the 1416// folder_view. Here we handle hover open issues for subfolders. 1417// Caution: there are subtle differences between this one and 1418// bookmark_bar_controller.mm's version. 1419- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { 1420 NSPoint currentLocation = [info draggingLocation]; 1421 BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation]; 1422 1423 // Don't allow drops that would result in cycles. 1424 if (button) { 1425 NSData* data = [[info draggingPasteboard] 1426 dataForType:kBookmarkButtonDragType]; 1427 if (data && [info draggingSource]) { 1428 BookmarkButton* sourceButton = nil; 1429 [data getBytes:&sourceButton length:sizeof(sourceButton)]; 1430 const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; 1431 const BookmarkNode* destNode = [button bookmarkNode]; 1432 if (destNode->HasAncestor(sourceNode)) 1433 button = nil; 1434 } 1435 } 1436 // Delegate handling of dragging over a button to the |hoverState_| member. 1437 return [hoverState_ draggingEnteredButton:button]; 1438} 1439 1440- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info { 1441 return NSDragOperationMove; 1442} 1443 1444// Unlike bookmark_bar_controller, we need to keep track of dragging state. 1445// We also need to make sure we cancel the delayed hover close. 1446- (void)draggingExited:(id<NSDraggingInfo>)info { 1447 // NOT the same as a cancel --> we may have moved the mouse into the submenu. 1448 // Delegate handling of the hover button to the |hoverState_| member. 1449 [hoverState_ draggingExited]; 1450} 1451 1452- (BOOL)dragShouldLockBarVisibility { 1453 return [parentController_ dragShouldLockBarVisibility]; 1454} 1455 1456// TODO(jrg): ARGH more code dup. 1457// http://crbug.com/35966 1458- (BOOL)dragButton:(BookmarkButton*)sourceButton 1459 to:(NSPoint)point 1460 copy:(BOOL)copy { 1461 DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); 1462 const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; 1463 return [self dragBookmark:sourceNode to:point copy:copy]; 1464} 1465 1466// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController. 1467// http://crbug.com/35966 1468- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { 1469 BOOL dragged = NO; 1470 std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]); 1471 if (nodes.size()) { 1472 BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); 1473 NSPoint dropPoint = [info draggingLocation]; 1474 for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin(); 1475 it != nodes.end(); ++it) { 1476 const BookmarkNode* sourceNode = *it; 1477 dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; 1478 } 1479 } 1480 return dragged; 1481} 1482 1483// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController. 1484// http://crbug.com/35966 1485- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { 1486 std::vector<const BookmarkNode*> dragDataNodes; 1487 BookmarkNodeData dragData; 1488 if (dragData.ReadFromClipboard(ui::CLIPBOARD_TYPE_DRAG)) { 1489 BookmarkModel* bookmarkModel = [self bookmarkModel]; 1490 std::vector<const BookmarkNode*> nodes( 1491 dragData.GetNodes(bookmarkModel, profile_->GetPath())); 1492 dragDataNodes.assign(nodes.begin(), nodes.end()); 1493 } 1494 return dragDataNodes; 1495} 1496 1497// Return YES if we should show the drop indicator, else NO. 1498// TODO(jrg): ARGH code dup! 1499// http://crbug.com/35966 1500- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { 1501 return ![self buttonForDroppingOnAtPoint:point]; 1502} 1503 1504// Button selection change code to support type to select and arrow key events. 1505#pragma mark Keyboard Support 1506 1507// Scroll the menu to show the selected button, if it's not already visible. 1508- (void)showSelectedButton { 1509 int bMaxIndex = [self buttonCount] - 1; // Max array index in button array. 1510 1511 // Is there a valid selected button? 1512 if (bMaxIndex < 0 || selectedIndex_ < 0 || selectedIndex_ > bMaxIndex) 1513 return; 1514 1515 // Is the menu scrollable anyway? 1516 if (![self canScrollUp] && ![self canScrollDown]) 1517 return; 1518 1519 // Now check to see if we need to scroll, which way, and how far. 1520 CGFloat delta = 0.0; 1521 NSPoint scrollPoint = [scrollView_ documentVisibleRect].origin; 1522 CGFloat itemBottom = (bMaxIndex - selectedIndex_) * 1523 bookmarks::kBookmarkFolderButtonHeight; 1524 CGFloat itemTop = itemBottom + bookmarks::kBookmarkFolderButtonHeight; 1525 CGFloat viewHeight = NSHeight([scrollView_ frame]); 1526 1527 if (scrollPoint.y > itemBottom) { // Need to scroll down. 1528 delta = scrollPoint.y - itemBottom; 1529 } else if ((scrollPoint.y + viewHeight) < itemTop) { // Need to scroll up. 1530 delta = -(itemTop - (scrollPoint.y + viewHeight)); 1531 } else { // No need to scroll. 1532 return; 1533 } 1534 1535 [self performOneScroll:delta]; 1536} 1537 1538// All changes to selectedness of buttons (aka fake menu items) ends up 1539// calling this method to actually flip the state of items. 1540// Needs to handle -1 as the invalid index (when nothing is selected) and 1541// greater than range values too. 1542- (void)setStateOfButtonByIndex:(int)index 1543 state:(bool)state { 1544 if (index >= 0 && index < [self buttonCount]) 1545 [[buttons_ objectAtIndex:index] highlight:state]; 1546} 1547 1548// Selects the required button and deselects the previously selected one. 1549// An index of -1 means no selection. 1550- (void)setSelectedButtonByIndex:(int)index { 1551 if (index == selectedIndex_) 1552 return; 1553 1554 [self setStateOfButtonByIndex:selectedIndex_ state:NO]; 1555 [self setStateOfButtonByIndex:index state:YES]; 1556 selectedIndex_ = index; 1557 1558 [self showSelectedButton]; 1559} 1560 1561- (void)clearInputText { 1562 [typedPrefix_ release]; 1563 typedPrefix_ = nil; 1564} 1565 1566// Find the earliest item in the folder which has the target prefix. 1567// Returns nil if there is no prefix or there are no matches. 1568// These are in no particular order, and not particularly numerous, so linear 1569// search should be OK. 1570// -1 means no match. 1571- (int)earliestBookmarkIndexWithPrefix:(NSString*)prefix { 1572 if ([prefix length] == 0) // Also handles nil. 1573 return -1; 1574 int maxButtons = [buttons_ count]; 1575 NSString* lowercasePrefix = [prefix lowercaseString]; 1576 for (int i = 0 ; i < maxButtons ; ++i) { 1577 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1578 if ([[[button title] lowercaseString] hasPrefix:lowercasePrefix]) 1579 return i; 1580 } 1581 return -1; 1582} 1583 1584- (void)setSelectedButtonByPrefix:(NSString*)prefix { 1585 [self setSelectedButtonByIndex:[self earliestBookmarkIndexWithPrefix:prefix]]; 1586} 1587 1588- (void)selectPrevious { 1589 int newIndex; 1590 if (selectedIndex_ == 0) 1591 return; 1592 if (selectedIndex_ < 0) 1593 newIndex = [self buttonCount] -1; 1594 else 1595 newIndex = std::max(selectedIndex_ - 1, 0); 1596 [self setSelectedButtonByIndex:newIndex]; 1597} 1598 1599- (void)selectNext { 1600 if (selectedIndex_ + 1 < [self buttonCount]) 1601 [self setSelectedButtonByIndex:selectedIndex_ + 1]; 1602} 1603 1604- (BOOL)handleInputText:(NSString*)newText { 1605 const unichar kUnicodeEscape = 0x001B; 1606 const unichar kUnicodeSpace = 0x0020; 1607 1608 // Event goes to the deepest nested open submenu. 1609 if (folderController_) 1610 return [folderController_ handleInputText:newText]; 1611 1612 // Look for arrow keys or other function keys. 1613 if ([newText length] == 1) { 1614 // Get the 16-bit unicode char. 1615 unichar theChar = [newText characterAtIndex:0]; 1616 switch (theChar) { 1617 1618 // Keys that trigger opening of the selection. 1619 case kUnicodeSpace: // Space. 1620 case NSNewlineCharacter: 1621 case NSCarriageReturnCharacter: 1622 case NSEnterCharacter: 1623 if (selectedIndex_ >= 0 && selectedIndex_ < [self buttonCount]) { 1624 [barController_ openBookmark:[buttons_ objectAtIndex:selectedIndex_]]; 1625 return NO; // NO because the selection-handling code will close later. 1626 } else { 1627 return YES; // Triggering with no selection closes the menu. 1628 } 1629 // Keys that cancel and close the menu. 1630 case kUnicodeEscape: 1631 case NSDeleteCharacter: 1632 case NSBackspaceCharacter: 1633 [self clearInputText]; 1634 return YES; 1635 // Keys that change selection directionally. 1636 case NSUpArrowFunctionKey: 1637 [self clearInputText]; 1638 [self selectPrevious]; 1639 return NO; 1640 case NSDownArrowFunctionKey: 1641 [self clearInputText]; 1642 [self selectNext]; 1643 return NO; 1644 // Keys that open and close submenus. 1645 case NSRightArrowFunctionKey: { 1646 BookmarkButton* btn = [self buttonAtIndex:selectedIndex_]; 1647 if (btn && [btn isFolder]) { 1648 [self openBookmarkFolderFromButtonAndCloseOldOne:btn]; 1649 [folderController_ selectNext]; 1650 } 1651 [self clearInputText]; 1652 return NO; 1653 } 1654 case NSLeftArrowFunctionKey: 1655 [self clearInputText]; 1656 [parentController_ closeBookmarkFolder:self]; 1657 return NO; 1658 1659 // Check for other keys that should close the menu. 1660 default: { 1661 if (theChar > NSUpArrowFunctionKey && 1662 theChar <= NSModeSwitchFunctionKey) { 1663 [self clearInputText]; 1664 return YES; 1665 } 1666 break; 1667 } 1668 } 1669 } 1670 1671 // It is a char or string worth adding to the type-select buffer. 1672 NSString* newString = (!typedPrefix_) ? 1673 newText : [typedPrefix_ stringByAppendingString:newText]; 1674 [typedPrefix_ release]; 1675 typedPrefix_ = [newString retain]; 1676 [self setSelectedButtonByPrefix:typedPrefix_]; 1677 return NO; 1678} 1679 1680// Return the y position for a drop indicator. 1681// 1682// TODO(jrg): again we have code dup, sort of, with 1683// bookmark_bar_controller.mm, but the axis is changed. 1684// http://crbug.com/35966 1685- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { 1686 CGFloat y = 0; 1687 int destIndex = [self indexForDragToPoint:point]; 1688 int numButtons = static_cast<int>([buttons_ count]); 1689 1690 // If it's a drop strictly between existing buttons or at the very beginning 1691 if (destIndex >= 0 && destIndex < numButtons) { 1692 // ... put the indicator right between the buttons. 1693 BookmarkButton* button = 1694 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)]; 1695 DCHECK(button); 1696 NSRect buttonFrame = [button frame]; 1697 y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding; 1698 1699 // If it's a drop at the end (past the last button, if there are any) ... 1700 } else if (destIndex == numButtons) { 1701 // and if it's past the last button ... 1702 if (numButtons > 0) { 1703 // ... find the last button, and put the indicator below it. 1704 BookmarkButton* button = 1705 [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; 1706 DCHECK(button); 1707 NSRect buttonFrame = [button frame]; 1708 y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding; 1709 1710 } 1711 } else { 1712 NOTREACHED(); 1713 } 1714 1715 return y; 1716} 1717 1718- (ThemeService*)themeService { 1719 return [parentController_ themeService]; 1720} 1721 1722- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { 1723 // Do nothing. 1724} 1725 1726- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { 1727 // Do nothing. 1728} 1729 1730- (BookmarkBarFolderController*)folderController { 1731 return folderController_; 1732} 1733 1734- (void)faviconLoadedForNode:(const BookmarkNode*)node { 1735 for (BookmarkButton* button in buttons_.get()) { 1736 if ([button bookmarkNode] == node) { 1737 [button setImage:[barController_ faviconForNode:node]]; 1738 [button setNeedsDisplay:YES]; 1739 return; 1740 } 1741 } 1742 1743 // Node was not in this menu, try submenu. 1744 if (folderController_) 1745 [folderController_ faviconLoadedForNode:node]; 1746} 1747 1748// Add a new folder controller as triggered by the given folder button. 1749- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { 1750 if (folderController_) 1751 [self closeBookmarkFolder:self]; 1752 1753 // Folder controller, like many window controllers, owns itself. 1754 folderController_ = 1755 [[BookmarkBarFolderController alloc] initWithParentButton:parentButton 1756 parentController:self 1757 barController:barController_ 1758 profile:profile_]; 1759 [folderController_ showWindow:self]; 1760} 1761 1762- (void)openAll:(const BookmarkNode*)node 1763 disposition:(WindowOpenDisposition)disposition { 1764 [barController_ openAll:node disposition:disposition]; 1765} 1766 1767- (void)addButtonForNode:(const BookmarkNode*)node 1768 atIndex:(NSInteger)buttonIndex { 1769 // Propose the frame for the new button. By default, this will be set to the 1770 // topmost button's frame (and there will always be one) offset upward in 1771 // anticipation of insertion. 1772 NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame]; 1773 newButtonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight; 1774 // When adding a button to an empty folder we must remove the 'empty' 1775 // placeholder button. This can be detected by checking for a parent 1776 // child count of 1. 1777 const BookmarkNode* parentNode = node->parent(); 1778 if (parentNode->child_count() == 1) { 1779 BookmarkButton* emptyButton = [buttons_ lastObject]; 1780 newButtonFrame = [emptyButton frame]; 1781 [emptyButton setDelegate:nil]; 1782 [emptyButton removeFromSuperview]; 1783 [buttons_ removeLastObject]; 1784 } 1785 1786 if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count]) 1787 buttonIndex = [buttons_ count]; 1788 1789 // Offset upward by one button height all buttons above insertion location. 1790 BookmarkButton* button = nil; // Remember so it can be de-highlighted. 1791 for (NSInteger i = 0; i < buttonIndex; ++i) { 1792 button = [buttons_ objectAtIndex:i]; 1793 // Remember this location in case it's the last button being moved 1794 // which is where the new button will be located. 1795 newButtonFrame = [button frame]; 1796 NSRect buttonFrame = [button frame]; 1797 buttonFrame.origin.y += bookmarks::kBookmarkFolderButtonHeight; 1798 [button setFrame:buttonFrame]; 1799 } 1800 [[button cell] mouseExited:nil]; // De-highlight. 1801 BookmarkButton* newButton = [self makeButtonForNode:node 1802 frame:newButtonFrame]; 1803 [buttons_ insertObject:newButton atIndex:buttonIndex]; 1804 [folderView_ addSubview:newButton]; 1805 1806 // Close any child folder(s) which may still be open. 1807 [self closeBookmarkFolder:self]; 1808 1809 [self adjustWindowForButtonCount:[buttons_ count]]; 1810} 1811 1812// More code which essentially duplicates that of BookmarkBarController. 1813// TODO(mrossetti,jrg): http://crbug.com/35966 1814- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { 1815 DCHECK([urls count] == [titles count]); 1816 BOOL nodesWereAdded = NO; 1817 // Figure out where these new bookmarks nodes are to be added. 1818 BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; 1819 BookmarkModel* bookmarkModel = [self bookmarkModel]; 1820 const BookmarkNode* destParent = NULL; 1821 int destIndex = 0; 1822 if ([button isFolder]) { 1823 destParent = [button bookmarkNode]; 1824 // Drop it at the end. 1825 destIndex = [button bookmarkNode]->child_count(); 1826 } else { 1827 // Else we're dropping somewhere in the folder, so find the right spot. 1828 destParent = [parentButton_ bookmarkNode]; 1829 destIndex = [self indexForDragToPoint:point]; 1830 // Be careful if the number of buttons != number of nodes. 1831 destIndex += [[parentButton_ cell] startingChildIndex]; 1832 } 1833 1834 ChromeBookmarkClient* client = 1835 ChromeBookmarkClientFactory::GetForProfile(profile_); 1836 if (!client->CanBeEditedByUser(destParent)) 1837 return NO; 1838 1839 // Create and add the new bookmark nodes. 1840 size_t urlCount = [urls count]; 1841 for (size_t i = 0; i < urlCount; ++i) { 1842 GURL gurl; 1843 const char* string = [[urls objectAtIndex:i] UTF8String]; 1844 if (string) 1845 gurl = GURL(string); 1846 // We only expect to receive valid URLs. 1847 DCHECK(gurl.is_valid()); 1848 if (gurl.is_valid()) { 1849 bookmarkModel->AddURL(destParent, 1850 destIndex++, 1851 base::SysNSStringToUTF16([titles objectAtIndex:i]), 1852 gurl); 1853 nodesWereAdded = YES; 1854 } 1855 } 1856 return nodesWereAdded; 1857} 1858 1859- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { 1860 if (fromIndex != toIndex) { 1861 if (toIndex == -1) 1862 toIndex = [buttons_ count]; 1863 BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; 1864 if (movedButton == buttonThatMouseIsIn_) 1865 buttonThatMouseIsIn_ = nil; 1866 [buttons_ removeObjectAtIndex:fromIndex]; 1867 NSRect movedFrame = [movedButton frame]; 1868 NSPoint toOrigin = movedFrame.origin; 1869 [movedButton setHidden:YES]; 1870 if (fromIndex < toIndex) { 1871 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1]; 1872 toOrigin = [targetButton frame].origin; 1873 for (NSInteger i = fromIndex; i < toIndex; ++i) { 1874 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1875 NSRect frame = [button frame]; 1876 frame.origin.y += bookmarks::kBookmarkFolderButtonHeight; 1877 [button setFrameOrigin:frame.origin]; 1878 } 1879 } else { 1880 BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex]; 1881 toOrigin = [targetButton frame].origin; 1882 for (NSInteger i = fromIndex - 1; i >= toIndex; --i) { 1883 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1884 NSRect buttonFrame = [button frame]; 1885 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight; 1886 [button setFrameOrigin:buttonFrame.origin]; 1887 } 1888 } 1889 [buttons_ insertObject:movedButton atIndex:toIndex]; 1890 [movedButton setFrameOrigin:toOrigin]; 1891 [movedButton setHidden:NO]; 1892 } 1893} 1894 1895// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966 1896- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { 1897 // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360 1898 BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; 1899 NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation]; 1900 1901 // If this button has an open sub-folder, close it. 1902 if ([folderController_ parentButton] == oldButton) 1903 [self closeBookmarkFolder:self]; 1904 1905 // If a hover-open is pending, cancel it. 1906 if (oldButton == buttonThatMouseIsIn_) { 1907 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 1908 buttonThatMouseIsIn_ = nil; 1909 } 1910 1911 // Deleting a button causes rearrangement that enables us to lose a 1912 // mouse-exited event. This problem doesn't appear to exist with 1913 // other keep-menu-open options (e.g. add folder). Since the 1914 // showsBorderOnlyWhileMouseInside uses a tracking area, simple 1915 // tricks (e.g. sending an extra mouseExited: to the button) don't 1916 // fix the problem. 1917 // http://crbug.com/54324 1918 for (NSButton* button in buttons_.get()) { 1919 if ([button showsBorderOnlyWhileMouseInside]) { 1920 [button setShowsBorderOnlyWhileMouseInside:NO]; 1921 [button setShowsBorderOnlyWhileMouseInside:YES]; 1922 } 1923 } 1924 1925 [oldButton setDelegate:nil]; 1926 [oldButton removeFromSuperview]; 1927 [buttons_ removeObjectAtIndex:buttonIndex]; 1928 for (NSInteger i = 0; i < buttonIndex; ++i) { 1929 BookmarkButton* button = [buttons_ objectAtIndex:i]; 1930 NSRect buttonFrame = [button frame]; 1931 buttonFrame.origin.y -= bookmarks::kBookmarkFolderButtonHeight; 1932 [button setFrame:buttonFrame]; 1933 } 1934 // Search for and adjust submenus, if necessary. 1935 NSInteger buttonCount = [buttons_ count]; 1936 if (buttonCount) { 1937 BookmarkButton* subButton = [folderController_ parentButton]; 1938 for (NSButton* aButton in buttons_.get()) { 1939 // If this button is showing its menu then we need to move the menu, too. 1940 if (aButton == subButton) 1941 [folderController_ 1942 offsetFolderMenuWindow:NSMakeSize(0.0, chrome::kBookmarkBarHeight)]; 1943 } 1944 } else if (parentButton_ != [barController_ otherBookmarksButton]) { 1945 // If all nodes have been removed from this folder then add in the 1946 // 'empty' placeholder button except for "Other bookmarks" folder 1947 // as we are going to hide it. 1948 NSRect buttonFrame = 1949 GetFirstButtonFrameForHeight([self menuHeightForButtonCount:1]); 1950 BookmarkButton* button = [self makeButtonForNode:nil 1951 frame:buttonFrame]; 1952 [buttons_ addObject:button]; 1953 [folderView_ addSubview:button]; 1954 buttonCount = 1; 1955 } 1956 1957 // buttonCount will be 0 if "Other bookmarks" folder is empty, so close 1958 // the folder before hiding it. 1959 if (buttonCount == 0) 1960 [barController_ closeBookmarkFolder:nil]; 1961 else if (buttonCount > 0) 1962 [self adjustWindowForButtonCount:buttonCount]; 1963 1964 if (animate && !ignoreAnimations_) 1965 NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, 1966 NSZeroSize, nil, nil, nil); 1967} 1968 1969- (id<BookmarkButtonControllerProtocol>)controllerForNode: 1970 (const BookmarkNode*)node { 1971 // See if we are holding this node, otherwise see if it is in our 1972 // hierarchy of visible folder menus. 1973 if ([parentButton_ bookmarkNode] == node) 1974 return self; 1975 return [folderController_ controllerForNode:node]; 1976} 1977 1978#pragma mark TestingAPI Only 1979 1980- (BOOL)canScrollUp { 1981 return ![scrollUpArrowView_ isHidden]; 1982} 1983 1984- (BOOL)canScrollDown { 1985 return ![scrollDownArrowView_ isHidden]; 1986} 1987 1988- (CGFloat)verticalScrollArrowHeight { 1989 return verticalScrollArrowHeight_; 1990} 1991 1992- (NSView*)visibleView { 1993 return visibleView_; 1994} 1995 1996- (NSScrollView*)scrollView { 1997 return scrollView_; 1998} 1999 2000- (NSView*)folderView { 2001 return folderView_; 2002} 2003 2004- (void)setIgnoreAnimations:(BOOL)ignore { 2005 ignoreAnimations_ = ignore; 2006} 2007 2008- (BookmarkButton*)buttonThatMouseIsIn { 2009 return buttonThatMouseIsIn_; 2010} 2011 2012@end // BookmarkBarFolderController 2013