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