- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / extensions / browser_actions_controller.mm
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/chrome_notification_types.h"
13 #include "chrome/browser/extensions/extension_action.h"
14 #include "chrome/browser/extensions/extension_action_manager.h"
15 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/extensions/extension_system.h"
17 #include "chrome/browser/extensions/extension_toolbar_model.h"
18 #include "chrome/browser/extensions/extension_util.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/sessions/session_tab_helper.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
24 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
25 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
26 #import "chrome/browser/ui/cocoa/image_button_cell.h"
27 #import "chrome/browser/ui/cocoa/menu_button.h"
28 #include "chrome/browser/ui/tabs/tab_strip_model.h"
29 #include "chrome/common/extensions/api/extension_action/action_info.h"
30 #include "chrome/common/pref_names.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
38 using extensions::Extension;
39 using extensions::ExtensionList;
40
41 NSString* const kBrowserActionVisibilityChangedNotification =
42     @"BrowserActionVisibilityChangedNotification";
43
44 namespace {
45 const CGFloat kAnimationDuration = 0.2;
46
47 const 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.
52 const CGFloat kBrowserActionOriginYOffset = 5.0;
53
54 // The size of each button on the toolbar.
55 const CGFloat kBrowserActionHeight = 29.0;
56 const CGFloat kBrowserActionWidth = 29.0;
57
58 // The padding between browser action buttons.
59 const 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.
63 const 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.
67 const 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 and whether
142 // it should grant tab permissions. API-simulated clicks should not grant.
143 - (BOOL)browserActionClicked:(BrowserActionButton*)button
144                  shouldGrant:(BOOL)shouldGrant;
145 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
146
147 // Returns whether the given extension should be displayed. Only displays
148 // incognito-enabled extensions in incognito mode. Otherwise returns YES.
149 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension;
150
151 // The reason |frame| is specified in these chevron functions is because the
152 // container may be animating and the end frame of the animation should be
153 // passed instead of the current frame (which may be off and cause the chevron
154 // to jump at the end of its animation).
155
156 // Shows the overflow chevron button depending on whether there are any hidden
157 // extensions within the frame given.
158 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate;
159
160 // Moves the chevron to its correct position within |frame|.
161 - (void)updateChevronPositionInFrame:(NSRect)frame;
162
163 // Shows or hides the chevron, animating as specified by |animate|.
164 - (void)setChevronHidden:(BOOL)hidden
165                  inFrame:(NSRect)frame
166                  animate:(BOOL)animate;
167
168 // Handles when a menu item within the chevron overflow menu is selected.
169 - (void)chevronItemSelected:(id)menuItem;
170
171 // Updates the container's grippy cursor based on the number of hidden buttons.
172 - (void)updateGrippyCursors;
173
174 // Returns the ID of the currently selected tab or -1 if none exists.
175 - (int)currentTabId;
176 @end
177
178 // A helper class to proxy extension notifications to the view controller's
179 // appropriate methods.
180 class ExtensionServiceObserverBridge : public content::NotificationObserver,
181                                        public ExtensionToolbarModel::Observer {
182  public:
183   ExtensionServiceObserverBridge(BrowserActionsController* owner,
184                                  Browser* browser)
185     : owner_(owner), browser_(browser) {
186     registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
187                    content::Source<Profile>(browser->profile()));
188     registrar_.Add(this,
189                    chrome::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 chrome::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 chrome::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         ExtensionService* service = browser_->profile()->GetExtensionService();
215         if (!service)
216           break;
217         const Extension* extension = service->GetExtensionById(extension_id,
218                                                                false);
219         if (!extension)
220           break;
221         BrowserActionButton* button = [owner_ buttonForExtension:extension];
222         // |button| can be nil when the browser action has its button hidden.
223         if (button)
224           [owner_ browserActionClicked:button];
225         break;
226       }
227       default:
228         NOTREACHED() << L"Unexpected notification";
229     }
230   }
231
232   // ExtensionToolbarModel::Observer implementation.
233   virtual void BrowserActionAdded(
234       const Extension* extension,
235       int index) OVERRIDE {
236     [owner_ createActionButtonForExtension:extension withIndex:index];
237     [owner_ resizeContainerAndAnimate:NO];
238   }
239
240   virtual void BrowserActionRemoved(const Extension* extension) OVERRIDE {
241     [owner_ removeActionButtonForExtension:extension];
242     [owner_ resizeContainerAndAnimate:NO];
243   }
244
245   virtual bool BrowserActionShowPopup(const Extension* extension) OVERRIDE {
246     // Do not override other popups and only show in active window.
247     ExtensionPopupController* popup = [ExtensionPopupController popup];
248     if (popup || !browser_->window()->IsActive())
249       return false;
250
251     BrowserActionButton* button = [owner_ buttonForExtension:extension];
252     return button && [owner_ browserActionClicked:button
253                                       shouldGrant:NO];
254   }
255
256  private:
257   // The object we need to inform when we get a notification. Weak. Owns us.
258   BrowserActionsController* owner_;
259
260   // The browser we listen for events from. Weak.
261   Browser* browser_;
262
263   // Used for registering to receive notifications and automatic clean up.
264   content::NotificationRegistrar registrar_;
265
266   DISALLOW_COPY_AND_ASSIGN(ExtensionServiceObserverBridge);
267 };
268
269 @implementation BrowserActionsController
270
271 @synthesize containerView = containerView_;
272
273 #pragma mark -
274 #pragma mark Public Methods
275
276 - (id)initWithBrowser:(Browser*)browser
277         containerView:(BrowserActionsContainerView*)container {
278   DCHECK(browser && container);
279
280   if ((self = [super init])) {
281     browser_ = browser;
282     profile_ = browser->profile();
283
284     observer_.reset(new ExtensionServiceObserverBridge(self, browser_));
285     ExtensionService* extensionService =
286         extensions::ExtensionSystem::Get(profile_)->extension_service();
287     // |extensionService| can be NULL in Incognito.
288     if (extensionService) {
289       toolbarModel_ = extensionService->toolbar_model();
290       toolbarModel_->AddObserver(observer_.get());
291     }
292
293     containerView_ = container;
294     [containerView_ setPostsFrameChangedNotifications:YES];
295     [[NSNotificationCenter defaultCenter]
296         addObserver:self
297            selector:@selector(containerFrameChanged:)
298                name:NSViewFrameDidChangeNotification
299              object:containerView_];
300     [[NSNotificationCenter defaultCenter]
301         addObserver:self
302            selector:@selector(containerDragStart:)
303                name:kBrowserActionGrippyDragStartedNotification
304              object:containerView_];
305     [[NSNotificationCenter defaultCenter]
306         addObserver:self
307            selector:@selector(containerDragging:)
308                name:kBrowserActionGrippyDraggingNotification
309              object:containerView_];
310     [[NSNotificationCenter defaultCenter]
311         addObserver:self
312            selector:@selector(containerDragFinished:)
313                name:kBrowserActionGrippyDragFinishedNotification
314              object:containerView_];
315     // Listen for a finished drag from any button to make sure each open window
316     // stays in sync.
317     [[NSNotificationCenter defaultCenter]
318       addObserver:self
319          selector:@selector(actionButtonDragFinished:)
320              name:kBrowserActionButtonDragEndNotification
321            object:nil];
322
323     chevronAnimation_.reset([[NSViewAnimation alloc] init]);
324     [chevronAnimation_ gtm_setDuration:kAnimationDuration
325                              eventMask:NSLeftMouseUpMask];
326     [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
327
328     hiddenButtons_.reset([[NSMutableArray alloc] init]);
329     buttons_.reset([[NSMutableDictionary alloc] init]);
330     [self createButtons];
331     [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
332     [self updateGrippyCursors];
333     [container setResizable:!profile_->IsOffTheRecord()];
334   }
335
336   return self;
337 }
338
339 - (void)dealloc {
340   if (toolbarModel_)
341     toolbarModel_->RemoveObserver(observer_.get());
342
343   [[NSNotificationCenter defaultCenter] removeObserver:self];
344   [super dealloc];
345 }
346
347 - (void)update {
348   for (BrowserActionButton* button in [buttons_ allValues]) {
349     [button setTabId:[self currentTabId]];
350     [button updateState];
351   }
352 }
353
354 - (NSUInteger)buttonCount {
355   return [buttons_ count];
356 }
357
358 - (NSUInteger)visibleButtonCount {
359   return [self buttonCount] - [hiddenButtons_ count];
360 }
361
362 - (void)resizeContainerAndAnimate:(BOOL)animate {
363   int iconCount = toolbarModel_->GetVisibleIconCount();
364   if (iconCount < 0)  // If no buttons are hidden.
365     iconCount = [self buttonCount];
366
367   [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
368                         animate:animate];
369   NSRect frame = animate ? [containerView_ animationEndFrame] :
370                            [containerView_ frame];
371
372   [self showChevronIfNecessaryInFrame:frame animate:animate];
373
374   if (!animate) {
375     [[NSNotificationCenter defaultCenter]
376         postNotificationName:kBrowserActionVisibilityChangedNotification
377                       object:self];
378   }
379 }
380
381 - (NSView*)browserActionViewForExtension:(const Extension*)extension {
382   for (BrowserActionButton* button in [buttons_ allValues]) {
383     if ([button extension] == extension)
384       return button;
385   }
386   NOTREACHED();
387   return nil;
388 }
389
390 - (CGFloat)savedWidth {
391   if (!toolbarModel_)
392     return 0;
393   if (!profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) {
394     // Migration code to the new VisibleIconCount pref.
395     // TODO(mpcomplete): remove this at some point.
396     double predefinedWidth =
397         profile_->GetPrefs()->GetDouble(prefs::kBrowserActionContainerWidth);
398     if (predefinedWidth != 0) {
399       int iconWidth = kBrowserActionWidth + kBrowserActionButtonPadding;
400       int extraWidth = kChevronWidth;
401       toolbarModel_->SetVisibleIconCount(
402           (predefinedWidth - extraWidth) / iconWidth);
403     }
404   }
405
406   int savedButtonCount = toolbarModel_->GetVisibleIconCount();
407   if (savedButtonCount < 0 ||  // all icons are visible
408       static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
409     savedButtonCount = [self buttonCount];
410   return [self containerWidthWithButtonCount:savedButtonCount];
411 }
412
413 - (NSPoint)popupPointForBrowserAction:(const Extension*)extension {
414   if (!extensions::ExtensionActionManager::Get(profile_)->
415       GetBrowserAction(*extension)) {
416     return NSZeroPoint;
417   }
418
419   NSButton* button = [self buttonForExtension:extension];
420   if (!button)
421     return NSZeroPoint;
422
423   if ([hiddenButtons_ containsObject:button])
424     button = chevronMenuButton_.get();
425
426   // Anchor point just above the center of the bottom.
427   const NSRect bounds = [button bounds];
428   DCHECK([button isFlipped]);
429   NSPoint anchor = NSMakePoint(NSMidX(bounds),
430                                NSMaxY(bounds) - kBrowserActionBubbleYOffset);
431   return [button convertPoint:anchor toView:nil];
432 }
433
434 - (BOOL)chevronIsHidden {
435   if (!chevronMenuButton_.get())
436     return YES;
437
438   if (![chevronAnimation_ isAnimating])
439     return [chevronMenuButton_ isHidden];
440
441   DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
442
443   // The chevron is animating in or out. Determine which one and have the return
444   // value reflect where the animation is headed.
445   NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
446       valueForKey:NSViewAnimationEffectKey];
447   if (effect == NSViewAnimationFadeInEffect) {
448     return NO;
449   } else if (effect == NSViewAnimationFadeOutEffect) {
450     return YES;
451   }
452
453   NOTREACHED();
454   return YES;
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->get()])
491       continue;
492
493     [self createActionButtonForExtension:iter->get() 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->get()])
583       continue;
584     BrowserActionButton* button = [self buttonForExtension:(iter->get())];
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->get())];
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->get())];
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 - (BOOL)browserActionClicked:(BrowserActionButton*)button
758                  shouldGrant:(BOOL)shouldGrant {
759   const Extension* extension = [button extension];
760   GURL popupUrl;
761   switch (toolbarModel_->ExecuteBrowserAction(extension, browser_, &popupUrl,
762                                               shouldGrant)) {
763     case ExtensionToolbarModel::ACTION_NONE:
764       break;
765     case ExtensionToolbarModel::ACTION_SHOW_POPUP: {
766       NSPoint arrowPoint = [self popupPointForBrowserAction:extension];
767       [ExtensionPopupController showURL:popupUrl
768                               inBrowser:browser_
769                              anchoredAt:arrowPoint
770                           arrowLocation:info_bubble::kTopRight
771                                 devMode:NO];
772       return YES;
773     }
774   }
775   return NO;
776 }
777
778 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
779   return [self browserActionClicked:button
780                         shouldGrant:YES];
781 }
782
783 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
784   // Only display incognito-enabled extensions while in incognito mode.
785   return
786       (!profile_->IsOffTheRecord() ||
787        extension_util::IsIncognitoEnabled(
788           extension->id(),
789           extensions::ExtensionSystem::Get(profile_)->extension_service()));
790 }
791
792 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
793   [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
794                  inFrame:frame
795                  animate:animate];
796 }
797
798 - (void)updateChevronPositionInFrame:(NSRect)frame {
799   CGFloat xPos = NSWidth(frame) - kChevronWidth;
800   NSRect buttonFrame = NSMakeRect(xPos,
801                                   kBrowserActionOriginYOffset,
802                                   kChevronWidth,
803                                   kBrowserActionHeight);
804   [chevronMenuButton_ setFrame:buttonFrame];
805 }
806
807 - (void)setChevronHidden:(BOOL)hidden
808                  inFrame:(NSRect)frame
809                  animate:(BOOL)animate {
810   if (hidden == [self chevronIsHidden])
811     return;
812
813   if (!chevronMenuButton_.get()) {
814     chevronMenuButton_.reset([[MenuButton alloc] init]);
815     [chevronMenuButton_ setOpenMenuOnClick:YES];
816     [chevronMenuButton_ setBordered:NO];
817     [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
818
819     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
820                            forButtonState:image_button_cell::kDefaultState];
821     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
822                            forButtonState:image_button_cell::kHoverState];
823     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
824                            forButtonState:image_button_cell::kPressedState];
825
826     overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
827     [overflowMenu_ setAutoenablesItems:NO];
828     [overflowMenu_ setDelegate:self];
829     [chevronMenuButton_ setAttachedMenu:overflowMenu_];
830
831     [containerView_ addSubview:chevronMenuButton_];
832   }
833
834   [self updateChevronPositionInFrame:frame];
835
836   // Stop any running animation.
837   [chevronAnimation_ stopAnimation];
838
839   if (!animate) {
840     [chevronMenuButton_ setHidden:hidden];
841     return;
842   }
843
844   NSDictionary* animationDictionary;
845   if (hidden) {
846     animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
847         chevronMenuButton_.get(), NSViewAnimationTargetKey,
848         NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
849         nil];
850   } else {
851     [chevronMenuButton_ setHidden:NO];
852     animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
853         chevronMenuButton_.get(), NSViewAnimationTargetKey,
854         NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
855         nil];
856   }
857   [chevronAnimation_ setViewAnimations:
858       [NSArray arrayWithObject:animationDictionary]];
859   [chevronAnimation_ startAnimation];
860 }
861
862 - (void)chevronItemSelected:(id)menuItem {
863   [self browserActionClicked:[menuItem representedObject]];
864 }
865
866 - (void)updateGrippyCursors {
867   [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
868   [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
869   [[containerView_ window] invalidateCursorRectsForView:containerView_];
870 }
871
872 - (int)currentTabId {
873   content::WebContents* active_tab =
874       browser_->tab_strip_model()->GetActiveWebContents();
875   if (!active_tab)
876     return -1;
877
878   return SessionTabHelper::FromWebContents(active_tab)->session_id().id();
879 }
880
881 #pragma mark -
882 #pragma mark Testing Methods
883
884 - (NSButton*)buttonWithIndex:(NSUInteger)index {
885   if (profile_->IsOffTheRecord())
886     index = toolbarModel_->IncognitoIndexToOriginal(index);
887   const extensions::ExtensionList& toolbar_items =
888       toolbarModel_->toolbar_items();
889   if (index < toolbar_items.size()) {
890     const Extension* extension = toolbar_items[index].get();
891     return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];
892   }
893   return nil;
894 }
895
896 @end