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 "browser_actions_controller.h" 6 7 #include <cmath> 8 #include <string> 9 10 #include "app/mac/nsimage_cache.h" 11 #include "base/sys_string_conversions.h" 12 #include "chrome/browser/extensions/extension_browser_event_router.h" 13 #include "chrome/browser/extensions/extension_host.h" 14 #include "chrome/browser/extensions/extension_service.h" 15 #include "chrome/browser/extensions/extension_toolbar_model.h" 16 #include "chrome/browser/prefs/pref_service.h" 17 #include "chrome/browser/profiles/profile.h" 18 #include "chrome/browser/ui/browser.h" 19 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h" 20 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h" 21 #import "chrome/browser/ui/cocoa/extensions/chevron_menu_button.h" 22 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h" 23 #import "chrome/browser/ui/cocoa/menu_button.h" 24 #include "chrome/common/extensions/extension_action.h" 25 #include "chrome/common/pref_names.h" 26 #include "content/browser/tab_contents/tab_contents.h" 27 #include "content/common/notification_details.h" 28 #include "content/common/notification_observer.h" 29 #include "content/common/notification_registrar.h" 30 #include "content/common/notification_source.h" 31 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" 32 33 NSString* const kBrowserActionVisibilityChangedNotification = 34 @"BrowserActionVisibilityChangedNotification"; 35 36 namespace { 37 const CGFloat kAnimationDuration = 0.2; 38 39 const CGFloat kChevronWidth = 14.0; 40 41 // Image used for the overflow button. 42 NSString* const kOverflowChevronsName = 43 @"browser_actions_overflow_Template.pdf"; 44 45 // Since the container is the maximum height of the toolbar, we have 46 // to move the buttons up by this amount in order to have them look 47 // vertically centered within the toolbar. 48 const CGFloat kBrowserActionOriginYOffset = 5.0; 49 50 // The size of each button on the toolbar. 51 const CGFloat kBrowserActionHeight = 29.0; 52 const CGFloat kBrowserActionWidth = 29.0; 53 54 // The padding between browser action buttons. 55 const CGFloat kBrowserActionButtonPadding = 2.0; 56 57 // Padding between Omnibox and first button. Since the buttons have a 58 // pixel of internal padding, this needs an extra pixel. 59 const CGFloat kBrowserActionLeftPadding = kBrowserActionButtonPadding + 1.0; 60 61 // How far to inset from the bottom of the view to get the top border 62 // of the popup 2px below the bottom of the Omnibox. 63 const CGFloat kBrowserActionBubbleYOffset = 3.0; 64 65 } // namespace 66 67 @interface BrowserActionsController(Private) 68 // Used during initialization to create the BrowserActionButton objects from the 69 // stored toolbar model. 70 - (void)createButtons; 71 72 // Creates and then adds the given extension's action button to the container 73 // at the given index within the container. It does not affect the toolbar model 74 // object since it is called when the toolbar model changes. 75 - (void)createActionButtonForExtension:(const Extension*)extension 76 withIndex:(NSUInteger)index; 77 78 // Removes an action button for the given extension from the container. This 79 // method also does not affect the underlying toolbar model since it is called 80 // when the toolbar model changes. 81 - (void)removeActionButtonForExtension:(const Extension*)extension; 82 83 // Useful in the case of a Browser Action being added/removed from the middle of 84 // the container, this method repositions each button according to the current 85 // toolbar model. 86 - (void)positionActionButtonsAndAnimate:(BOOL)animate; 87 88 // During container resizing, buttons become more transparent as they are pushed 89 // off the screen. This method updates each button's opacity determined by the 90 // position of the button. 91 - (void)updateButtonOpacity; 92 93 // Returns the existing button with the given extension backing it; nil if it 94 // cannot be found or the extension's ID is invalid. 95 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension; 96 97 // Returns the preferred width of the container given the number of visible 98 // buttons |buttonCount|. 99 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount; 100 101 // Returns the number of buttons that can fit in the container according to its 102 // current size. 103 - (NSUInteger)containerButtonCapacity; 104 105 // Notification handlers for events registered by the class. 106 107 // Updates each button's opacity, the cursor rects and chevron position. 108 - (void)containerFrameChanged:(NSNotification*)notification; 109 110 // Hides the chevron and unhides every hidden button so that dragging the 111 // container out smoothly shows the Browser Action buttons. 112 - (void)containerDragStart:(NSNotification*)notification; 113 114 // Sends a notification for the toolbar to reposition surrounding UI elements. 115 - (void)containerDragging:(NSNotification*)notification; 116 117 // Determines which buttons need to be hidden based on the new size, hides them 118 // and updates the chevron overflow menu. Also fires a notification to let the 119 // toolbar know that the drag has finished. 120 - (void)containerDragFinished:(NSNotification*)notification; 121 122 // Updates the image associated with the button should it be within the chevron 123 // menu. 124 - (void)actionButtonUpdated:(NSNotification*)notification; 125 126 // Adjusts the position of the surrounding action buttons depending on where the 127 // button is within the container. 128 - (void)actionButtonDragging:(NSNotification*)notification; 129 130 // Updates the position of the Browser Actions within the container. This fires 131 // when _any_ Browser Action button is done dragging to keep all open windows in 132 // sync visually. 133 - (void)actionButtonDragFinished:(NSNotification*)notification; 134 135 // Moves the given button both visually and within the toolbar model to the 136 // specified index. 137 - (void)moveButton:(BrowserActionButton*)button 138 toIndex:(NSUInteger)index 139 animate:(BOOL)animate; 140 141 // Handles when the given BrowserActionButton object is clicked. 142 - (void)browserActionClicked:(BrowserActionButton*)button; 143 144 // Returns whether the given extension should be displayed. Only displays 145 // incognito-enabled extensions in incognito mode. Otherwise returns YES. 146 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension; 147 148 // The reason |frame| is specified in these chevron functions is because the 149 // container may be animating and the end frame of the animation should be 150 // passed instead of the current frame (which may be off and cause the chevron 151 // to jump at the end of its animation). 152 153 // Shows the overflow chevron button depending on whether there are any hidden 154 // extensions within the frame given. 155 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate; 156 157 // Moves the chevron to its correct position within |frame|. 158 - (void)updateChevronPositionInFrame:(NSRect)frame; 159 160 // Shows or hides the chevron, animating as specified by |animate|. 161 - (void)setChevronHidden:(BOOL)hidden 162 inFrame:(NSRect)frame 163 animate:(BOOL)animate; 164 165 // Handles when a menu item within the chevron overflow menu is selected. 166 - (void)chevronItemSelected:(id)menuItem; 167 168 // Clears and then populates the overflow menu based on the contents of 169 // |hiddenButtons_|. 170 - (void)updateOverflowMenu; 171 172 // Updates the container's grippy cursor based on the number of hidden buttons. 173 - (void)updateGrippyCursors; 174 175 // Returns the ID of the currently selected tab or -1 if none exists. 176 - (int)currentTabId; 177 @end 178 179 // A helper class to proxy extension notifications to the view controller's 180 // appropriate methods. 181 class ExtensionServiceObserverBridge : public NotificationObserver, 182 public ExtensionToolbarModel::Observer { 183 public: 184 ExtensionServiceObserverBridge(BrowserActionsController* owner, 185 Profile* profile) : owner_(owner) { 186 registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE, 187 Source<Profile>(profile)); 188 } 189 190 // Overridden from NotificationObserver. 191 void Observe(NotificationType type, 192 const NotificationSource& source, 193 const NotificationDetails& details) { 194 switch (type.value) { 195 case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: { 196 ExtensionPopupController* popup = [ExtensionPopupController popup]; 197 if (popup && ![popup isClosing]) 198 [popup close]; 199 200 break; 201 } 202 default: 203 NOTREACHED() << L"Unexpected notification"; 204 } 205 } 206 207 // ExtensionToolbarModel::Observer implementation. 208 void BrowserActionAdded(const Extension* extension, int index) { 209 [owner_ createActionButtonForExtension:extension withIndex:index]; 210 [owner_ resizeContainerAndAnimate:NO]; 211 } 212 213 void BrowserActionRemoved(const Extension* extension) { 214 [owner_ removeActionButtonForExtension:extension]; 215 [owner_ resizeContainerAndAnimate:NO]; 216 } 217 218 private: 219 // The object we need to inform when we get a notification. Weak. Owns us. 220 BrowserActionsController* owner_; 221 222 // Used for registering to receive notifications and automatic clean up. 223 NotificationRegistrar registrar_; 224 225 DISALLOW_COPY_AND_ASSIGN(ExtensionServiceObserverBridge); 226 }; 227 228 @implementation BrowserActionsController 229 230 @synthesize containerView = containerView_; 231 232 #pragma mark - 233 #pragma mark Public Methods 234 235 - (id)initWithBrowser:(Browser*)browser 236 containerView:(BrowserActionsContainerView*)container { 237 DCHECK(browser && container); 238 239 if ((self = [super init])) { 240 browser_ = browser; 241 profile_ = browser->profile(); 242 243 if (!profile_->GetPrefs()->FindPreference( 244 prefs::kBrowserActionContainerWidth)) 245 [BrowserActionsController registerUserPrefs:profile_->GetPrefs()]; 246 247 observer_.reset(new ExtensionServiceObserverBridge(self, profile_)); 248 ExtensionService* extensionsService = profile_->GetExtensionService(); 249 // |extensionsService| can be NULL in Incognito. 250 if (extensionsService) { 251 toolbarModel_ = extensionsService->toolbar_model(); 252 toolbarModel_->AddObserver(observer_.get()); 253 } 254 255 containerView_ = container; 256 [containerView_ setPostsFrameChangedNotifications:YES]; 257 [[NSNotificationCenter defaultCenter] 258 addObserver:self 259 selector:@selector(containerFrameChanged:) 260 name:NSViewFrameDidChangeNotification 261 object:containerView_]; 262 [[NSNotificationCenter defaultCenter] 263 addObserver:self 264 selector:@selector(containerDragStart:) 265 name:kBrowserActionGrippyDragStartedNotification 266 object:containerView_]; 267 [[NSNotificationCenter defaultCenter] 268 addObserver:self 269 selector:@selector(containerDragging:) 270 name:kBrowserActionGrippyDraggingNotification 271 object:containerView_]; 272 [[NSNotificationCenter defaultCenter] 273 addObserver:self 274 selector:@selector(containerDragFinished:) 275 name:kBrowserActionGrippyDragFinishedNotification 276 object:containerView_]; 277 // Listen for a finished drag from any button to make sure each open window 278 // stays in sync. 279 [[NSNotificationCenter defaultCenter] 280 addObserver:self 281 selector:@selector(actionButtonDragFinished:) 282 name:kBrowserActionButtonDragEndNotification 283 object:nil]; 284 285 chevronAnimation_.reset([[NSViewAnimation alloc] init]); 286 [chevronAnimation_ gtm_setDuration:kAnimationDuration 287 eventMask:NSLeftMouseUpMask]; 288 [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking]; 289 290 hiddenButtons_.reset([[NSMutableArray alloc] init]); 291 buttons_.reset([[NSMutableDictionary alloc] init]); 292 [self createButtons]; 293 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO]; 294 [self updateGrippyCursors]; 295 [container setResizable:!profile_->IsOffTheRecord()]; 296 } 297 298 return self; 299 } 300 301 - (void)dealloc { 302 if (toolbarModel_) 303 toolbarModel_->RemoveObserver(observer_.get()); 304 305 [[NSNotificationCenter defaultCenter] removeObserver:self]; 306 [super dealloc]; 307 } 308 309 - (void)update { 310 for (BrowserActionButton* button in [buttons_ allValues]) { 311 [button setTabId:[self currentTabId]]; 312 [button updateState]; 313 } 314 } 315 316 - (NSUInteger)buttonCount { 317 return [buttons_ count]; 318 } 319 320 - (NSUInteger)visibleButtonCount { 321 return [self buttonCount] - [hiddenButtons_ count]; 322 } 323 324 - (MenuButton*)chevronMenuButton { 325 return chevronMenuButton_.get(); 326 } 327 328 - (void)resizeContainerAndAnimate:(BOOL)animate { 329 int iconCount = toolbarModel_->GetVisibleIconCount(); 330 if (iconCount < 0) // If no buttons are hidden. 331 iconCount = [self buttonCount]; 332 333 [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount] 334 animate:animate]; 335 NSRect frame = animate ? [containerView_ animationEndFrame] : 336 [containerView_ frame]; 337 338 [self showChevronIfNecessaryInFrame:frame animate:animate]; 339 340 if (!animate) { 341 [[NSNotificationCenter defaultCenter] 342 postNotificationName:kBrowserActionVisibilityChangedNotification 343 object:self]; 344 } 345 } 346 347 - (NSView*)browserActionViewForExtension:(const Extension*)extension { 348 for (BrowserActionButton* button in [buttons_ allValues]) { 349 if ([button extension] == extension) 350 return button; 351 } 352 NOTREACHED(); 353 return nil; 354 } 355 356 - (CGFloat)savedWidth { 357 if (!toolbarModel_) 358 return 0; 359 if (!profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) { 360 // Migration code to the new VisibleIconCount pref. 361 // TODO(mpcomplete): remove this at some point. 362 double predefinedWidth = 363 profile_->GetPrefs()->GetDouble(prefs::kBrowserActionContainerWidth); 364 if (predefinedWidth != 0) { 365 int iconWidth = kBrowserActionWidth + kBrowserActionButtonPadding; 366 int extraWidth = kChevronWidth; 367 toolbarModel_->SetVisibleIconCount( 368 (predefinedWidth - extraWidth) / iconWidth); 369 } 370 } 371 372 int savedButtonCount = toolbarModel_->GetVisibleIconCount(); 373 if (savedButtonCount < 0 || // all icons are visible 374 static_cast<NSUInteger>(savedButtonCount) > [self buttonCount]) 375 savedButtonCount = [self buttonCount]; 376 return [self containerWidthWithButtonCount:savedButtonCount]; 377 } 378 379 - (NSPoint)popupPointForBrowserAction:(const Extension*)extension { 380 if (!extension->browser_action()) 381 return NSZeroPoint; 382 383 NSButton* button = [self buttonForExtension:extension]; 384 if (!button) 385 return NSZeroPoint; 386 387 if ([hiddenButtons_ containsObject:button]) 388 button = chevronMenuButton_.get(); 389 390 // Anchor point just above the center of the bottom. 391 const NSRect bounds = [button bounds]; 392 DCHECK([button isFlipped]); 393 NSPoint anchor = NSMakePoint(NSMidX(bounds), 394 NSMaxY(bounds) - kBrowserActionBubbleYOffset); 395 return [button convertPoint:anchor toView:nil]; 396 } 397 398 - (BOOL)chevronIsHidden { 399 if (!chevronMenuButton_.get()) 400 return YES; 401 402 if (![chevronAnimation_ isAnimating]) 403 return [chevronMenuButton_ isHidden]; 404 405 DCHECK([[chevronAnimation_ viewAnimations] count] > 0); 406 407 // The chevron is animating in or out. Determine which one and have the return 408 // value reflect where the animation is headed. 409 NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0] 410 valueForKey:NSViewAnimationEffectKey]; 411 if (effect == NSViewAnimationFadeInEffect) { 412 return NO; 413 } else if (effect == NSViewAnimationFadeOutEffect) { 414 return YES; 415 } 416 417 NOTREACHED(); 418 return YES; 419 } 420 421 + (void)registerUserPrefs:(PrefService*)prefs { 422 prefs->RegisterDoublePref(prefs::kBrowserActionContainerWidth, 0); 423 } 424 425 #pragma mark - 426 #pragma mark Private Methods 427 428 - (void)createButtons { 429 if (!toolbarModel_) 430 return; 431 432 NSUInteger i = 0; 433 for (ExtensionList::iterator iter = toolbarModel_->begin(); 434 iter != toolbarModel_->end(); ++iter) { 435 if (![self shouldDisplayBrowserAction:*iter]) 436 continue; 437 438 [self createActionButtonForExtension:*iter withIndex:i++]; 439 } 440 441 [[NSNotificationCenter defaultCenter] 442 addObserver:self 443 selector:@selector(actionButtonUpdated:) 444 name:kBrowserActionButtonUpdatedNotification 445 object:nil]; 446 447 CGFloat width = [self savedWidth]; 448 [containerView_ resizeToWidth:width animate:NO]; 449 } 450 451 - (void)createActionButtonForExtension:(const Extension*)extension 452 withIndex:(NSUInteger)index { 453 if (!extension->browser_action()) 454 return; 455 456 if (![self shouldDisplayBrowserAction:extension]) 457 return; 458 459 if (profile_->IsOffTheRecord()) 460 index = toolbarModel_->OriginalIndexToIncognito(index); 461 462 // Show the container if it's the first button. Otherwise it will be shown 463 // already. 464 if ([self buttonCount] == 0) 465 [containerView_ setHidden:NO]; 466 467 NSRect buttonFrame = NSMakeRect(0.0, kBrowserActionOriginYOffset, 468 kBrowserActionWidth, kBrowserActionHeight); 469 BrowserActionButton* newButton = 470 [[[BrowserActionButton alloc] 471 initWithFrame:buttonFrame 472 extension:extension 473 profile:profile_ 474 tabId:[self currentTabId]] autorelease]; 475 [newButton setTarget:self]; 476 [newButton setAction:@selector(browserActionClicked:)]; 477 NSString* buttonKey = base::SysUTF8ToNSString(extension->id()); 478 if (!buttonKey) 479 return; 480 [buttons_ setObject:newButton forKey:buttonKey]; 481 482 [self positionActionButtonsAndAnimate:NO]; 483 484 [[NSNotificationCenter defaultCenter] 485 addObserver:self 486 selector:@selector(actionButtonDragging:) 487 name:kBrowserActionButtonDraggingNotification 488 object:newButton]; 489 490 491 [containerView_ setMaxWidth: 492 [self containerWidthWithButtonCount:[self buttonCount]]]; 493 [containerView_ setNeedsDisplay:YES]; 494 } 495 496 - (void)removeActionButtonForExtension:(const Extension*)extension { 497 if (!extension->browser_action()) 498 return; 499 500 NSString* buttonKey = base::SysUTF8ToNSString(extension->id()); 501 if (!buttonKey) 502 return; 503 504 BrowserActionButton* button = [buttons_ objectForKey:buttonKey]; 505 // This could be the case in incognito, where only a subset of extensions are 506 // shown. 507 if (!button) 508 return; 509 510 [button removeFromSuperview]; 511 // It may or may not be hidden, but it won't matter to NSMutableArray either 512 // way. 513 [hiddenButtons_ removeObject:button]; 514 [self updateOverflowMenu]; 515 516 [buttons_ removeObjectForKey:buttonKey]; 517 if ([self buttonCount] == 0) { 518 // No more buttons? Hide the container. 519 [containerView_ setHidden:YES]; 520 } else { 521 [self positionActionButtonsAndAnimate:NO]; 522 } 523 [containerView_ setMaxWidth: 524 [self containerWidthWithButtonCount:[self buttonCount]]]; 525 [containerView_ setNeedsDisplay:YES]; 526 } 527 528 - (void)positionActionButtonsAndAnimate:(BOOL)animate { 529 NSUInteger i = 0; 530 for (ExtensionList::iterator iter = toolbarModel_->begin(); 531 iter != toolbarModel_->end(); ++iter) { 532 if (![self shouldDisplayBrowserAction:*iter]) 533 continue; 534 BrowserActionButton* button = [self buttonForExtension:(*iter)]; 535 if (!button) 536 continue; 537 if (![button isBeingDragged]) 538 [self moveButton:button toIndex:i animate:animate]; 539 ++i; 540 } 541 } 542 543 - (void)updateButtonOpacity { 544 for (BrowserActionButton* button in [buttons_ allValues]) { 545 NSRect buttonFrame = [button frame]; 546 if (NSContainsRect([containerView_ bounds], buttonFrame)) { 547 if ([button alphaValue] != 1.0) 548 [button setAlphaValue:1.0]; 549 550 continue; 551 } 552 CGFloat intersectionWidth = 553 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame)); 554 CGFloat alpha = std::max(0.0f, intersectionWidth / NSWidth(buttonFrame)); 555 [button setAlphaValue:alpha]; 556 [button setNeedsDisplay:YES]; 557 } 558 } 559 560 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension { 561 NSString* extensionId = base::SysUTF8ToNSString(extension->id()); 562 DCHECK(extensionId); 563 if (!extensionId) 564 return nil; 565 return [buttons_ objectForKey:extensionId]; 566 } 567 568 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount { 569 // Left-side padding which works regardless of whether a button or 570 // chevron leads. 571 CGFloat width = kBrowserActionLeftPadding; 572 573 // Include the buttons and padding between. 574 if (buttonCount > 0) { 575 width += buttonCount * kBrowserActionWidth; 576 width += (buttonCount - 1) * kBrowserActionButtonPadding; 577 } 578 579 // Make room for the chevron if any buttons are hidden. 580 if ([self buttonCount] != [self visibleButtonCount]) { 581 // Chevron and buttons both include 1px padding w/in their bounds, 582 // so this leaves 2px between the last browser action and chevron, 583 // and also works right if the chevron is the only button. 584 width += kChevronWidth; 585 } 586 587 return width; 588 } 589 590 - (NSUInteger)containerButtonCapacity { 591 // Edge-to-edge span of the browser action buttons. 592 CGFloat actionSpan = [self savedWidth] - kBrowserActionLeftPadding; 593 594 // Add in some padding for the browser action on the end, then 595 // divide out to get the number of action buttons that fit. 596 return (actionSpan + kBrowserActionButtonPadding) / 597 (kBrowserActionWidth + kBrowserActionButtonPadding); 598 } 599 600 - (void)containerFrameChanged:(NSNotification*)notification { 601 [self updateButtonOpacity]; 602 [[containerView_ window] invalidateCursorRectsForView:containerView_]; 603 [self updateChevronPositionInFrame:[containerView_ frame]]; 604 } 605 606 - (void)containerDragStart:(NSNotification*)notification { 607 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES]; 608 while([hiddenButtons_ count] > 0) { 609 [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]]; 610 [hiddenButtons_ removeObjectAtIndex:0]; 611 } 612 } 613 614 - (void)containerDragging:(NSNotification*)notification { 615 [[NSNotificationCenter defaultCenter] 616 postNotificationName:kBrowserActionGrippyDraggingNotification 617 object:self]; 618 } 619 620 - (void)containerDragFinished:(NSNotification*)notification { 621 for (ExtensionList::iterator iter = toolbarModel_->begin(); 622 iter != toolbarModel_->end(); ++iter) { 623 BrowserActionButton* button = [self buttonForExtension:(*iter)]; 624 NSRect buttonFrame = [button frame]; 625 if (NSContainsRect([containerView_ bounds], buttonFrame)) 626 continue; 627 628 CGFloat intersectionWidth = 629 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame)); 630 // Pad the threshold by 5 pixels in order to have the buttons hide more 631 // easily. 632 if (([containerView_ grippyPinned] && intersectionWidth > 0) || 633 (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) { 634 [button setAlphaValue:0.0]; 635 [button removeFromSuperview]; 636 [hiddenButtons_ addObject:button]; 637 } 638 } 639 [self updateOverflowMenu]; 640 [self updateGrippyCursors]; 641 642 if (!profile_->IsOffTheRecord()) 643 toolbarModel_->SetVisibleIconCount([self visibleButtonCount]); 644 645 [[NSNotificationCenter defaultCenter] 646 postNotificationName:kBrowserActionGrippyDragFinishedNotification 647 object:self]; 648 } 649 650 - (void)actionButtonUpdated:(NSNotification*)notification { 651 BrowserActionButton* button = [notification object]; 652 if (![hiddenButtons_ containsObject:button]) 653 return; 654 655 // +1 item because of the title placeholder. See |updateOverflowMenu|. 656 NSUInteger menuIndex = [hiddenButtons_ indexOfObject:button] + 1; 657 NSMenuItem* item = [[chevronMenuButton_ attachedMenu] itemAtIndex:menuIndex]; 658 DCHECK(button == [item representedObject]); 659 [item setImage:[button compositedImage]]; 660 } 661 662 - (void)actionButtonDragging:(NSNotification*)notification { 663 if (![self chevronIsHidden]) 664 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES]; 665 666 // Determine what index the dragged button should lie in, alter the model and 667 // reposition the buttons. 668 CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2); 669 BrowserActionButton* draggedButton = [notification object]; 670 NSRect draggedButtonFrame = [draggedButton frame]; 671 672 NSUInteger index = 0; 673 for (ExtensionList::iterator iter = toolbarModel_->begin(); 674 iter != toolbarModel_->end(); ++iter) { 675 BrowserActionButton* button = [self buttonForExtension:(*iter)]; 676 CGFloat intersectionWidth = 677 NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame])); 678 679 if (intersectionWidth > dragThreshold && button != draggedButton && 680 ![button isAnimating] && index < [self visibleButtonCount]) { 681 toolbarModel_->MoveBrowserAction([draggedButton extension], index); 682 [self positionActionButtonsAndAnimate:YES]; 683 return; 684 } 685 ++index; 686 } 687 } 688 689 - (void)actionButtonDragFinished:(NSNotification*)notification { 690 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES]; 691 [self positionActionButtonsAndAnimate:YES]; 692 } 693 694 - (void)moveButton:(BrowserActionButton*)button 695 toIndex:(NSUInteger)index 696 animate:(BOOL)animate { 697 CGFloat xOffset = kBrowserActionLeftPadding + 698 (index * (kBrowserActionWidth + kBrowserActionButtonPadding)); 699 NSRect buttonFrame = [button frame]; 700 buttonFrame.origin.x = xOffset; 701 [button setFrame:buttonFrame animate:animate]; 702 703 if (index < [self containerButtonCapacity]) { 704 // Make sure the button is within the visible container. 705 if ([button superview] != containerView_) { 706 [containerView_ addSubview:button]; 707 [button setAlphaValue:1.0]; 708 [hiddenButtons_ removeObjectIdenticalTo:button]; 709 } 710 } else if (![hiddenButtons_ containsObject:button]) { 711 [hiddenButtons_ addObject:button]; 712 [button removeFromSuperview]; 713 [button setAlphaValue:0.0]; 714 [self updateOverflowMenu]; 715 } 716 } 717 718 - (void)browserActionClicked:(BrowserActionButton*)button { 719 int tabId = [self currentTabId]; 720 if (tabId < 0) { 721 NOTREACHED() << "No current tab."; 722 return; 723 } 724 725 ExtensionAction* action = [button extension]->browser_action(); 726 if (action->HasPopup(tabId)) { 727 GURL popupUrl = action->GetPopupUrl(tabId); 728 // If a popup is already showing, check if the popup URL is the same. If so, 729 // then close the popup. 730 ExtensionPopupController* popup = [ExtensionPopupController popup]; 731 if (popup && 732 [[popup window] isVisible] && 733 [popup extensionHost]->GetURL() == popupUrl) { 734 [popup close]; 735 return; 736 } 737 NSPoint arrowPoint = [self popupPointForBrowserAction:[button extension]]; 738 [ExtensionPopupController showURL:popupUrl 739 inBrowser:browser_ 740 anchoredAt:arrowPoint 741 arrowLocation:info_bubble::kTopRight 742 devMode:NO]; 743 } else { 744 ExtensionService* service = profile_->GetExtensionService(); 745 service->browser_event_router()->BrowserActionExecuted( 746 profile_, action->extension_id(), browser_); 747 } 748 } 749 750 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension { 751 // Only display incognito-enabled extensions while in incognito mode. 752 return 753 (!profile_->IsOffTheRecord() || 754 profile_->GetExtensionService()->IsIncognitoEnabled(extension->id())); 755 } 756 757 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate { 758 [self setChevronHidden:([self buttonCount] == [self visibleButtonCount]) 759 inFrame:frame 760 animate:animate]; 761 } 762 763 - (void)updateChevronPositionInFrame:(NSRect)frame { 764 CGFloat xPos = NSWidth(frame) - kChevronWidth; 765 NSRect buttonFrame = NSMakeRect(xPos, 766 kBrowserActionOriginYOffset, 767 kChevronWidth, 768 kBrowserActionHeight); 769 [chevronMenuButton_ setFrame:buttonFrame]; 770 } 771 772 - (void)setChevronHidden:(BOOL)hidden 773 inFrame:(NSRect)frame 774 animate:(BOOL)animate { 775 if (hidden == [self chevronIsHidden]) 776 return; 777 778 if (!chevronMenuButton_.get()) { 779 chevronMenuButton_.reset([[ChevronMenuButton alloc] init]); 780 [chevronMenuButton_ setBordered:NO]; 781 [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES]; 782 NSImage* chevronImage = 783 app::mac::GetCachedImageWithName(kOverflowChevronsName); 784 [chevronMenuButton_ setImage:chevronImage]; 785 [containerView_ addSubview:chevronMenuButton_]; 786 } 787 788 if (!hidden) 789 [self updateOverflowMenu]; 790 791 [self updateChevronPositionInFrame:frame]; 792 793 // Stop any running animation. 794 [chevronAnimation_ stopAnimation]; 795 796 if (!animate) { 797 [chevronMenuButton_ setHidden:hidden]; 798 return; 799 } 800 801 NSDictionary* animationDictionary; 802 if (hidden) { 803 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys: 804 chevronMenuButton_.get(), NSViewAnimationTargetKey, 805 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, 806 nil]; 807 } else { 808 [chevronMenuButton_ setHidden:NO]; 809 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys: 810 chevronMenuButton_.get(), NSViewAnimationTargetKey, 811 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, 812 nil]; 813 } 814 [chevronAnimation_ setViewAnimations: 815 [NSArray arrayWithObject:animationDictionary]]; 816 [chevronAnimation_ startAnimation]; 817 } 818 819 - (void)chevronItemSelected:(id)menuItem { 820 [self browserActionClicked:[menuItem representedObject]]; 821 } 822 823 - (void)updateOverflowMenu { 824 overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]); 825 // See menu_button.h for documentation on why this is needed. 826 [overflowMenu_ addItemWithTitle:@"" action:nil keyEquivalent:@""]; 827 828 for (BrowserActionButton* button in hiddenButtons_.get()) { 829 NSString* name = base::SysUTF8ToNSString([button extension]->name()); 830 NSMenuItem* item = 831 [overflowMenu_ addItemWithTitle:name 832 action:@selector(chevronItemSelected:) 833 keyEquivalent:@""]; 834 [item setRepresentedObject:button]; 835 [item setImage:[button compositedImage]]; 836 [item setTarget:self]; 837 } 838 [chevronMenuButton_ setAttachedMenu:overflowMenu_]; 839 } 840 841 - (void)updateGrippyCursors { 842 [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0]; 843 [containerView_ setCanDragRight:[self visibleButtonCount] > 0]; 844 [[containerView_ window] invalidateCursorRectsForView:containerView_]; 845 } 846 847 - (int)currentTabId { 848 TabContents* selected_tab = browser_->GetSelectedTabContents(); 849 if (!selected_tab) 850 return -1; 851 852 return selected_tab->controller().session_id().id(); 853 } 854 855 #pragma mark - 856 #pragma mark Testing Methods 857 858 - (NSButton*)buttonWithIndex:(NSUInteger)index { 859 if (profile_->IsOffTheRecord()) 860 index = toolbarModel_->IncognitoIndexToOriginal(index); 861 if (index < toolbarModel_->size()) { 862 const Extension* extension = toolbarModel_->GetExtensionByIndex(index); 863 return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())]; 864 } 865 return nil; 866 } 867 868 @end 869