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