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