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