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