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