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