browser_actions_controller.mm revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
1// Copyright (c) 2010 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_toolbar_model.h"
15#include "chrome/browser/extensions/extension_service.h"
16#include "chrome/browser/prefs/pref_service.h"
17#include "chrome/browser/profiles/profile.h"
18#include "chrome/browser/tab_contents/tab_contents.h"
19#include "chrome/browser/ui/browser.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/chevron_menu_button.h"
23#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
24#import "chrome/browser/ui/cocoa/menu_button.h"
25#include "chrome/common/extensions/extension_action.h"
26#include "chrome/common/notification_details.h"
27#include "chrome/common/notification_observer.h"
28#include "chrome/common/notification_registrar.h"
29#include "chrome/common/notification_source.h"
30#include "chrome/common/pref_names.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()->GetReal(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->RegisterRealPref(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    ExtensionBrowserEventRouter::GetInstance()->BrowserActionExecuted(
745       profile_, action->extension_id(), browser_);
746  }
747}
748
749- (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
750  // Only display incognito-enabled extensions while in incognito mode.
751  return (!profile_->IsOffTheRecord() ||
752          profile_->GetExtensionService()->IsIncognitoEnabled(extension));
753}
754
755- (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
756  [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
757                 inFrame:frame
758                 animate:animate];
759}
760
761- (void)updateChevronPositionInFrame:(NSRect)frame {
762  CGFloat xPos = NSWidth(frame) - kChevronWidth;
763  NSRect buttonFrame = NSMakeRect(xPos,
764                                  kBrowserActionOriginYOffset,
765                                  kChevronWidth,
766                                  kBrowserActionHeight);
767  [chevronMenuButton_ setFrame:buttonFrame];
768}
769
770- (void)setChevronHidden:(BOOL)hidden
771                 inFrame:(NSRect)frame
772                 animate:(BOOL)animate {
773  if (hidden == [self chevronIsHidden])
774    return;
775
776  if (!chevronMenuButton_.get()) {
777    chevronMenuButton_.reset([[ChevronMenuButton alloc] init]);
778    [chevronMenuButton_ setBordered:NO];
779    [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
780    NSImage* chevronImage =
781        app::mac::GetCachedImageWithName(kOverflowChevronsName);
782    [chevronMenuButton_ setImage:chevronImage];
783    [containerView_ addSubview:chevronMenuButton_];
784  }
785
786  if (!hidden)
787    [self updateOverflowMenu];
788
789  [self updateChevronPositionInFrame:frame];
790
791  // Stop any running animation.
792  [chevronAnimation_ stopAnimation];
793
794  if (!animate) {
795    [chevronMenuButton_ setHidden:hidden];
796    return;
797  }
798
799  NSDictionary* animationDictionary;
800  if (hidden) {
801    animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
802        chevronMenuButton_.get(), NSViewAnimationTargetKey,
803        NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
804        nil];
805  } else {
806    [chevronMenuButton_ setHidden:NO];
807    animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
808        chevronMenuButton_.get(), NSViewAnimationTargetKey,
809        NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
810        nil];
811  }
812  [chevronAnimation_ setViewAnimations:
813      [NSArray arrayWithObject:animationDictionary]];
814  [chevronAnimation_ startAnimation];
815}
816
817- (void)chevronItemSelected:(id)menuItem {
818  [self browserActionClicked:[menuItem representedObject]];
819}
820
821- (void)updateOverflowMenu {
822  overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
823  // See menu_button.h for documentation on why this is needed.
824  [overflowMenu_ addItemWithTitle:@"" action:nil keyEquivalent:@""];
825
826  for (BrowserActionButton* button in hiddenButtons_.get()) {
827    NSString* name = base::SysUTF8ToNSString([button extension]->name());
828    NSMenuItem* item =
829        [overflowMenu_ addItemWithTitle:name
830                                 action:@selector(chevronItemSelected:)
831                          keyEquivalent:@""];
832    [item setRepresentedObject:button];
833    [item setImage:[button compositedImage]];
834    [item setTarget:self];
835  }
836  [chevronMenuButton_ setAttachedMenu:overflowMenu_];
837}
838
839- (void)updateGrippyCursors {
840  [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
841  [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
842  [[containerView_ window] invalidateCursorRectsForView:containerView_];
843}
844
845- (int)currentTabId {
846  TabContents* selected_tab = browser_->GetSelectedTabContents();
847  if (!selected_tab)
848    return -1;
849
850  return selected_tab->controller().session_id().id();
851}
852
853#pragma mark -
854#pragma mark Testing Methods
855
856- (NSButton*)buttonWithIndex:(NSUInteger)index {
857  if (profile_->IsOffTheRecord())
858    index = toolbarModel_->IncognitoIndexToOriginal(index);
859  if (index < toolbarModel_->size()) {
860    const Extension* extension = toolbarModel_->GetExtensionByIndex(index);
861    return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];
862  }
863  return nil;
864}
865
866@end
867