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