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 33NSString* const kBrowserActionVisibilityChangedNotification = 34 @"BrowserActionVisibilityChangedNotification"; 35 36namespace { 37const CGFloat kAnimationDuration = 0.2; 38 39const CGFloat kChevronWidth = 14.0; 40 41// Image used for the overflow button. 42NSString* 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. 48const CGFloat kBrowserActionOriginYOffset = 5.0; 49 50// The size of each button on the toolbar. 51const CGFloat kBrowserActionHeight = 29.0; 52const CGFloat kBrowserActionWidth = 29.0; 53 54// The padding between browser action buttons. 55const 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. 59const 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. 63const 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. 181class 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