browser_actions_container.cc revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1// Copyright (c) 2011 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#include "chrome/browser/ui/views/browser_actions_container.h"
6
7#include "base/stl_util-inl.h"
8#include "base/string_util.h"
9#include "base/utf_string_conversions.h"
10#include "chrome/browser/extensions/extension_browser_event_router.h"
11#include "chrome/browser/extensions/extension_host.h"
12#include "chrome/browser/extensions/extension_service.h"
13#include "chrome/browser/extensions/extension_tabs_module.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/ui/browser.h"
16#include "chrome/browser/ui/browser_window.h"
17#include "chrome/browser/ui/view_ids.h"
18#include "chrome/browser/ui/views/detachable_toolbar_view.h"
19#include "chrome/browser/ui/views/extensions/browser_action_drag_data.h"
20#include "chrome/browser/ui/views/extensions/extension_popup.h"
21#include "chrome/browser/ui/views/toolbar_view.h"
22#include "chrome/common/extensions/extension_action.h"
23#include "chrome/common/extensions/extension_resource.h"
24#include "chrome/common/pref_names.h"
25#include "content/browser/renderer_host/render_view_host.h"
26#include "content/browser/renderer_host/render_widget_host_view.h"
27#include "content/browser/tab_contents/tab_contents.h"
28#include "content/common/notification_source.h"
29#include "content/common/notification_type.h"
30#include "grit/app_resources.h"
31#include "grit/generated_resources.h"
32#include "third_party/skia/include/core/SkBitmap.h"
33#include "third_party/skia/include/effects/SkGradientShader.h"
34#include "ui/base/accessibility/accessible_view_state.h"
35#include "ui/base/animation/slide_animation.h"
36#include "ui/base/l10n/l10n_util.h"
37#include "ui/base/resource/resource_bundle.h"
38#include "ui/base/theme_provider.h"
39#include "ui/gfx/canvas.h"
40#include "ui/gfx/canvas_skia.h"
41#include "views/controls/button/menu_button.h"
42#include "views/controls/button/text_button.h"
43#include "views/controls/menu/menu_2.h"
44#include "views/drag_utils.h"
45#include "views/metrics.h"
46#include "views/window/window.h"
47
48#include "grit/theme_resources.h"
49
50// Horizontal spacing between most items in the container, as well as after the
51// last item or chevron (if visible).
52static const int kItemSpacing = ToolbarView::kStandardSpacing;
53// Horizontal spacing before the chevron (if visible).
54static const int kChevronSpacing = kItemSpacing - 2;
55
56// static
57bool BrowserActionsContainer::disable_animations_during_testing_ = false;
58
59////////////////////////////////////////////////////////////////////////////////
60// BrowserActionButton
61
62BrowserActionButton::BrowserActionButton(const Extension* extension,
63                                         BrowserActionsContainer* panel)
64    : ALLOW_THIS_IN_INITIALIZER_LIST(
65          MenuButton(this, std::wstring(), NULL, false)),
66      browser_action_(extension->browser_action()),
67      extension_(extension),
68      ALLOW_THIS_IN_INITIALIZER_LIST(tracker_(this)),
69      showing_context_menu_(false),
70      panel_(panel) {
71  set_border(NULL);
72  set_alignment(TextButton::ALIGN_CENTER);
73
74  // No UpdateState() here because View hierarchy not setup yet. Our parent
75  // should call UpdateState() after creation.
76
77  registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED,
78                 Source<ExtensionAction>(browser_action_));
79}
80
81void BrowserActionButton::Destroy() {
82  if (showing_context_menu_) {
83    context_menu_menu_->CancelMenu();
84    MessageLoop::current()->DeleteSoon(FROM_HERE, this);
85  } else {
86    delete this;
87  }
88}
89
90void BrowserActionButton::ViewHierarchyChanged(
91    bool is_add, View* parent, View* child) {
92  if (is_add && child == this) {
93    // The Browser Action API does not allow the default icon path to be
94    // changed at runtime, so we can load this now and cache it.
95    std::string relative_path = browser_action_->default_icon_path();
96    if (relative_path.empty())
97      return;
98
99    // LoadImage is not guaranteed to be synchronous, so we might see the
100    // callback OnImageLoaded execute immediately. It (through UpdateState)
101    // expects parent() to return the owner for this button, so this
102    // function is as early as we can start this request.
103    tracker_.LoadImage(extension_, extension_->GetResource(relative_path),
104                       gfx::Size(Extension::kBrowserActionIconMaxSize,
105                                 Extension::kBrowserActionIconMaxSize),
106                       ImageLoadingTracker::DONT_CACHE);
107  }
108
109  MenuButton::ViewHierarchyChanged(is_add, parent, child);
110}
111
112void BrowserActionButton::ButtonPressed(views::Button* sender,
113                                        const views::Event& event) {
114  panel_->OnBrowserActionExecuted(this, false);
115}
116
117void BrowserActionButton::OnImageLoaded(SkBitmap* image,
118                                        const ExtensionResource& resource,
119                                        int index) {
120  if (image)
121    default_icon_ = *image;
122
123  // Call back to UpdateState() because a more specific icon might have been set
124  // while the load was outstanding.
125  UpdateState();
126}
127
128void BrowserActionButton::UpdateState() {
129  int tab_id = panel_->GetCurrentTabId();
130  if (tab_id < 0)
131    return;
132
133  SkBitmap icon(browser_action()->GetIcon(tab_id));
134  if (icon.isNull())
135    icon = default_icon_;
136  if (!icon.isNull()) {
137    SkPaint paint;
138    paint.setXfermode(SkXfermode::Create(SkXfermode::kSrcOver_Mode));
139    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
140
141    SkBitmap bg;
142    rb.GetBitmapNamed(IDR_BROWSER_ACTION)->copyTo(&bg,
143        SkBitmap::kARGB_8888_Config);
144    SkCanvas bg_canvas(bg);
145    bg_canvas.drawBitmap(icon, SkIntToScalar((bg.width() - icon.width()) / 2),
146        SkIntToScalar((bg.height() - icon.height()) / 2), &paint);
147    SetIcon(bg);
148
149    SkBitmap bg_h;
150    rb.GetBitmapNamed(IDR_BROWSER_ACTION_H)->copyTo(&bg_h,
151        SkBitmap::kARGB_8888_Config);
152    SkCanvas bg_h_canvas(bg_h);
153    bg_h_canvas.drawBitmap(icon,
154        SkIntToScalar((bg_h.width() - icon.width()) / 2),
155        SkIntToScalar((bg_h.height() - icon.height()) / 2), &paint);
156    SetHoverIcon(bg_h);
157
158    SkBitmap bg_p;
159    rb.GetBitmapNamed(IDR_BROWSER_ACTION_P)->copyTo(&bg_p,
160        SkBitmap::kARGB_8888_Config);
161    SkCanvas bg_p_canvas(bg_p);
162    bg_p_canvas.drawBitmap(icon,
163        SkIntToScalar((bg_p.width() - icon.width()) / 2),
164        SkIntToScalar((bg_p.height() - icon.height()) / 2), &paint);
165    SetPushedIcon(bg_p);
166  }
167
168  // If the browser action name is empty, show the extension name instead.
169  string16 name = UTF8ToUTF16(browser_action()->GetTitle(tab_id));
170  if (name.empty())
171    name = UTF8ToUTF16(extension()->name());
172  SetTooltipText(UTF16ToWideHack(name));
173  parent()->SchedulePaint();
174}
175
176bool BrowserActionButton::IsPopup() {
177  int tab_id = panel_->GetCurrentTabId();
178  return (tab_id < 0) ? false : browser_action_->HasPopup(tab_id);
179}
180
181GURL BrowserActionButton::GetPopupUrl() {
182  int tab_id = panel_->GetCurrentTabId();
183  return (tab_id < 0) ? GURL() : browser_action_->GetPopupUrl(tab_id);
184}
185
186void BrowserActionButton::Observe(NotificationType type,
187                                  const NotificationSource& source,
188                                  const NotificationDetails& details) {
189  DCHECK(type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED);
190  UpdateState();
191  // The browser action may have become visible/hidden so we need to make
192  // sure the state gets updated.
193  panel_->OnBrowserActionVisibilityChanged();
194}
195
196bool BrowserActionButton::Activate() {
197  if (!IsPopup())
198    return true;
199
200  panel_->OnBrowserActionExecuted(this, false);
201
202  // TODO(erikkay): Run a nested modal loop while the mouse is down to
203  // enable menu-like drag-select behavior.
204
205  // The return value of this method is returned via OnMousePressed.
206  // We need to return false here since we're handing off focus to another
207  // widget/view, and true will grab it right back and try to send events
208  // to us.
209  return false;
210}
211
212bool BrowserActionButton::OnMousePressed(const views::MouseEvent& event) {
213  if (!event.IsRightMouseButton()) {
214    return IsPopup() ?
215        MenuButton::OnMousePressed(event) : TextButton::OnMousePressed(event);
216  }
217
218  // Get the top left point of this button in screen coordinates.
219  gfx::Point point = gfx::Point(0, 0);
220  ConvertPointToScreen(this, &point);
221
222  // Make the menu appear below the button.
223  point.Offset(0, height());
224
225  ShowContextMenu(point, true);
226  return false;
227}
228
229void BrowserActionButton::OnMouseReleased(const views::MouseEvent& event) {
230  if (IsPopup() || showing_context_menu_) {
231    // TODO(erikkay) this never actually gets called (probably because of the
232    // loss of focus).
233    MenuButton::OnMouseReleased(event);
234  } else {
235    TextButton::OnMouseReleased(event);
236  }
237}
238
239void BrowserActionButton::OnMouseExited(const views::MouseEvent& event) {
240  if (IsPopup() || showing_context_menu_)
241    MenuButton::OnMouseExited(event);
242  else
243    TextButton::OnMouseExited(event);
244}
245
246bool BrowserActionButton::OnKeyReleased(const views::KeyEvent& event) {
247  return IsPopup() ?
248      MenuButton::OnKeyReleased(event) : TextButton::OnKeyReleased(event);
249}
250
251void BrowserActionButton::ShowContextMenu(const gfx::Point& p,
252                                          bool is_mouse_gesture) {
253  if (!extension()->ShowConfigureContextMenus())
254    return;
255
256  showing_context_menu_ = true;
257  SetButtonPushed();
258
259  // Reconstructs the menu every time because the menu's contents are dynamic.
260  context_menu_contents_ =
261      new ExtensionContextMenuModel(extension(), panel_->browser(), panel_);
262  context_menu_menu_.reset(new views::Menu2(context_menu_contents_.get()));
263  context_menu_menu_->RunContextMenuAt(p);
264
265  SetButtonNotPushed();
266  showing_context_menu_ = false;
267}
268
269void BrowserActionButton::SetButtonPushed() {
270  SetState(views::CustomButton::BS_PUSHED);
271  menu_visible_ = true;
272}
273
274void BrowserActionButton::SetButtonNotPushed() {
275  SetState(views::CustomButton::BS_NORMAL);
276  menu_visible_ = false;
277}
278
279BrowserActionButton::~BrowserActionButton() {
280}
281
282
283////////////////////////////////////////////////////////////////////////////////
284// BrowserActionView
285
286BrowserActionView::BrowserActionView(const Extension* extension,
287                                     BrowserActionsContainer* panel)
288    : panel_(panel) {
289  button_ = new BrowserActionButton(extension, panel);
290  button_->SetDragController(panel_);
291  AddChildView(button_);
292  button_->UpdateState();
293}
294
295BrowserActionView::~BrowserActionView() {
296  RemoveChildView(button_);
297  button_->Destroy();
298}
299
300gfx::Canvas* BrowserActionView::GetIconWithBadge() {
301  int tab_id = panel_->GetCurrentTabId();
302
303  SkBitmap icon = button_->extension()->browser_action()->GetIcon(tab_id);
304  if (icon.isNull())
305    icon = button_->default_icon();
306
307  gfx::Canvas* canvas = new gfx::CanvasSkia(icon.width(), icon.height(), false);
308  canvas->DrawBitmapInt(icon, 0, 0);
309
310  if (tab_id >= 0) {
311    gfx::Rect bounds(icon.width(), icon.height() + ToolbarView::kVertSpacing);
312    button_->extension()->browser_action()->PaintBadge(canvas, bounds, tab_id);
313  }
314
315  return canvas;
316}
317
318void BrowserActionView::Layout() {
319  // We can't rely on button_->GetPreferredSize() here because that's not set
320  // correctly until the first call to
321  // BrowserActionsContainer::RefreshBrowserActionViews(), whereas this can be
322  // called before that when the initial bounds are set (and then not after,
323  // since the bounds don't change).  So instead of setting the height from the
324  // button's preferred size, we use IconHeight(), since that's how big the
325  // button should be regardless of what it's displaying.
326  button_->SetBounds(0, ToolbarView::kVertSpacing, width(),
327                     BrowserActionsContainer::IconHeight());
328}
329
330void BrowserActionView::GetAccessibleState(ui::AccessibleViewState* state) {
331  state->name = l10n_util::GetStringUTF16(
332      IDS_ACCNAME_EXTENSIONS_BROWSER_ACTION);
333  state->role = ui::AccessibilityTypes::ROLE_GROUPING;
334}
335
336void BrowserActionView::PaintChildren(gfx::Canvas* canvas) {
337  View::PaintChildren(canvas);
338  ExtensionAction* action = button()->browser_action();
339  int tab_id = panel_->GetCurrentTabId();
340  if (tab_id >= 0)
341    action->PaintBadge(canvas, gfx::Rect(width(), height()), tab_id);
342}
343
344////////////////////////////////////////////////////////////////////////////////
345// BrowserActionsContainer
346
347BrowserActionsContainer::BrowserActionsContainer(Browser* browser,
348                                                 View* owner_view)
349    : profile_(browser->profile()),
350      browser_(browser),
351      owner_view_(owner_view),
352      popup_(NULL),
353      popup_button_(NULL),
354      model_(NULL),
355      container_width_(0),
356      chevron_(NULL),
357      overflow_menu_(NULL),
358      suppress_chevron_(false),
359      resize_amount_(0),
360      animation_target_size_(0),
361      drop_indicator_position_(-1),
362      ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)),
363      ALLOW_THIS_IN_INITIALIZER_LIST(show_menu_task_factory_(this)) {
364  SetID(VIEW_ID_BROWSER_ACTION_TOOLBAR);
365
366  if (profile_->GetExtensionService()) {
367    model_ = profile_->GetExtensionService()->toolbar_model();
368    model_->AddObserver(this);
369  }
370
371  resize_animation_.reset(new ui::SlideAnimation(this));
372  resize_area_ = new views::ResizeArea(this);
373  AddChildView(resize_area_);
374
375  chevron_ = new views::MenuButton(NULL, std::wstring(), this, false);
376  chevron_->set_border(NULL);
377  chevron_->EnableCanvasFlippingForRTLUI(true);
378  chevron_->SetAccessibleName(
379      l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS_CHEVRON));
380  chevron_->SetVisible(false);
381  AddChildView(chevron_);
382}
383
384BrowserActionsContainer::~BrowserActionsContainer() {
385  if (model_)
386    model_->RemoveObserver(this);
387  StopShowFolderDropMenuTimer();
388  HidePopup();
389  DeleteBrowserActionViews();
390}
391
392// Static.
393void BrowserActionsContainer::RegisterUserPrefs(PrefService* prefs) {
394  prefs->RegisterIntegerPref(prefs::kBrowserActionContainerWidth, 0);
395}
396
397void BrowserActionsContainer::Init() {
398  LoadImages();
399
400  // We wait to set the container width until now so that the chevron images
401  // will be loaded.  The width calculation needs to know the chevron size.
402  if (model_ &&
403      !profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) {
404    // Migration code to the new VisibleIconCount pref.
405    // TODO(mpcomplete): remove this after users are upgraded to 5.0.
406    int predefined_width =
407        profile_->GetPrefs()->GetInteger(prefs::kBrowserActionContainerWidth);
408    if (predefined_width != 0)
409      model_->SetVisibleIconCount(WidthToIconCount(predefined_width));
410  }
411  if (model_ && model_->extensions_initialized())
412    SetContainerWidth();
413}
414
415int BrowserActionsContainer::GetCurrentTabId() const {
416  TabContents* tab_contents = browser_->GetSelectedTabContents();
417  return tab_contents ? tab_contents->controller().session_id().id() : -1;
418}
419
420BrowserActionView* BrowserActionsContainer::GetBrowserActionView(
421    ExtensionAction* action) {
422  for (BrowserActionViews::iterator iter = browser_action_views_.begin();
423       iter != browser_action_views_.end(); ++iter) {
424    if ((*iter)->button()->browser_action() == action)
425      return *iter;
426  }
427  return NULL;
428}
429
430void BrowserActionsContainer::RefreshBrowserActionViews() {
431  for (size_t i = 0; i < browser_action_views_.size(); ++i)
432    browser_action_views_[i]->button()->UpdateState();
433}
434
435void BrowserActionsContainer::CreateBrowserActionViews() {
436  DCHECK(browser_action_views_.empty());
437  if (!model_)
438    return;
439
440  for (ExtensionList::iterator iter = model_->begin(); iter != model_->end();
441       ++iter) {
442    if (!ShouldDisplayBrowserAction(*iter))
443      continue;
444
445    BrowserActionView* view = new BrowserActionView(*iter, this);
446    browser_action_views_.push_back(view);
447    AddChildView(view);
448  }
449}
450
451void BrowserActionsContainer::DeleteBrowserActionViews() {
452  if (!browser_action_views_.empty()) {
453    for (size_t i = 0; i < browser_action_views_.size(); ++i)
454      RemoveChildView(browser_action_views_[i]);
455    STLDeleteContainerPointers(browser_action_views_.begin(),
456                               browser_action_views_.end());
457    browser_action_views_.clear();
458  }
459}
460
461void BrowserActionsContainer::OnBrowserActionVisibilityChanged() {
462  SetVisible(!browser_action_views_.empty());
463  owner_view_->Layout();
464  owner_view_->SchedulePaint();
465}
466
467size_t BrowserActionsContainer::VisibleBrowserActions() const {
468  size_t visible_actions = 0;
469  for (size_t i = 0; i < browser_action_views_.size(); ++i) {
470    if (browser_action_views_[i]->IsVisible())
471      ++visible_actions;
472  }
473  return visible_actions;
474}
475
476void BrowserActionsContainer::OnBrowserActionExecuted(
477    BrowserActionButton* button,
478    bool inspect_with_devtools) {
479  ExtensionAction* browser_action = button->browser_action();
480
481  // Popups just display.  No notification to the extension.
482  // TODO(erikkay): should there be?
483  if (!button->IsPopup()) {
484    ExtensionService* service = profile_->GetExtensionService();
485    service->browser_event_router()->BrowserActionExecuted(
486        profile_, browser_action->extension_id(), browser_);
487    return;
488  }
489
490  // If we're showing the same popup, just hide it and return.
491  bool same_showing = popup_ && button == popup_button_;
492
493  // Always hide the current popup, even if it's not the same.
494  // Only one popup should be visible at a time.
495  HidePopup();
496
497  if (same_showing)
498    return;
499
500  // We can get the execute event for browser actions that are not visible,
501  // since buttons can be activated from the overflow menu (chevron). In that
502  // case we show the popup as originating from the chevron.
503  View* reference_view = button->parent()->IsVisible() ? button : chevron_;
504  gfx::Point origin;
505  View::ConvertPointToScreen(reference_view, &origin);
506  gfx::Rect rect = reference_view->bounds();
507  rect.set_origin(origin);
508
509  BubbleBorder::ArrowLocation arrow_location = base::i18n::IsRTL() ?
510      BubbleBorder::TOP_LEFT : BubbleBorder::TOP_RIGHT;
511
512  popup_ = ExtensionPopup::Show(button->GetPopupUrl(), browser_, rect,
513                                arrow_location, inspect_with_devtools,
514                                this);
515  popup_button_ = button;
516  popup_button_->SetButtonPushed();
517}
518
519gfx::Size BrowserActionsContainer::GetPreferredSize() {
520  if (browser_action_views_.empty())
521    return gfx::Size(ToolbarView::kStandardSpacing, 0);
522
523  // We calculate the size of the view by taking the current width and
524  // subtracting resize_amount_ (the latter represents how far the user is
525  // resizing the view or, if animating the snapping, how far to animate it).
526  // But we also clamp it to a minimum size and the maximum size, so that the
527  // container can never shrink too far or take up more space than it needs. In
528  // other words: ContainerMinSize() < width() - resize < ClampTo(MAX).
529  int clamped_width = std::min(
530      std::max(ContainerMinSize(), container_width_ - resize_amount_),
531      IconCountToWidth(-1, false));
532  return gfx::Size(clamped_width, 0);
533}
534
535void BrowserActionsContainer::Layout() {
536  if (browser_action_views_.empty()) {
537    SetVisible(false);
538    return;
539  }
540
541  SetVisible(true);
542  resize_area_->SetBounds(0, ToolbarView::kVertSpacing, kItemSpacing,
543                          IconHeight());
544
545  // If the icons don't all fit, show the chevron (unless suppressed).
546  int max_x = GetPreferredSize().width();
547  if ((IconCountToWidth(-1, false) > max_x) && !suppress_chevron_) {
548    chevron_->SetVisible(true);
549    gfx::Size chevron_size(chevron_->GetPreferredSize());
550    max_x -=
551        ToolbarView::kStandardSpacing + chevron_size.width() + kChevronSpacing;
552    chevron_->SetBounds(
553        width() - ToolbarView::kStandardSpacing - chevron_size.width(),
554        ToolbarView::kVertSpacing, chevron_size.width(), chevron_size.height());
555  } else {
556    chevron_->SetVisible(false);
557  }
558
559  // Now draw the icons for the browser actions in the available space.
560  int icon_width = IconWidth(false);
561  for (size_t i = 0; i < browser_action_views_.size(); ++i) {
562    BrowserActionView* view = browser_action_views_[i];
563    int x = ToolbarView::kStandardSpacing + (i * IconWidth(true));
564    if (x + icon_width <= max_x) {
565      view->SetBounds(x, 0, icon_width, height());
566      view->SetVisible(true);
567    } else {
568      view->SetVisible(false);
569    }
570  }
571}
572
573bool BrowserActionsContainer::GetDropFormats(
574    int* formats,
575    std::set<OSExchangeData::CustomFormat>* custom_formats) {
576  custom_formats->insert(BrowserActionDragData::GetBrowserActionCustomFormat());
577
578  return true;
579}
580
581bool BrowserActionsContainer::AreDropTypesRequired() {
582  return true;
583}
584
585bool BrowserActionsContainer::CanDrop(const OSExchangeData& data) {
586  BrowserActionDragData drop_data;
587  return drop_data.Read(data) ? drop_data.IsFromProfile(profile_) : false;
588}
589
590void BrowserActionsContainer::OnDragEntered(
591    const views::DropTargetEvent& event) {
592}
593
594int BrowserActionsContainer::OnDragUpdated(
595    const views::DropTargetEvent& event) {
596  // First check if we are above the chevron (overflow) menu.
597  if (GetEventHandlerForPoint(event.location()) == chevron_) {
598    if (show_menu_task_factory_.empty() && !overflow_menu_)
599      StartShowFolderDropMenuTimer();
600    return ui::DragDropTypes::DRAG_MOVE;
601  }
602  StopShowFolderDropMenuTimer();
603
604  // Figure out where to display the indicator.  This is a complex calculation:
605
606  // First, we figure out how much space is to the left of the icon area, so we
607  // can calculate the true offset into the icon area.
608  int width_before_icons = ToolbarView::kStandardSpacing +
609      (base::i18n::IsRTL() ?
610          (chevron_->GetPreferredSize().width() + kChevronSpacing) : 0);
611  int offset_into_icon_area = event.x() - width_before_icons;
612
613  // Next, we determine which icon to place the indicator in front of.  We want
614  // to place the indicator in front of icon n when the cursor is between the
615  // midpoints of icons (n - 1) and n.  To do this we take the offset into the
616  // icon area and transform it as follows:
617  //
618  // Real icon area:
619  //   0   a     *  b        c
620  //   |   |        |        |
621  //   |[IC|ON]  [IC|ON]  [IC|ON]
622  // We want to be before icon 0 for 0 < x <= a, icon 1 for a < x <= b, etc.
623  // Here the "*" represents the offset into the icon area, and since it's
624  // between a and b, we want to return "1".
625  //
626  // Transformed "icon area":
627  //   0        a     *  b        c
628  //   |        |        |        |
629  //   |[ICON]  |[ICON]  |[ICON]  |
630  // If we shift both our offset and our divider points later by half an icon
631  // plus one spacing unit, then it becomes very easy to calculate how many
632  // divider points we've passed, because they're the multiples of "one icon
633  // plus padding".
634  int before_icon_unclamped = (offset_into_icon_area + (IconWidth(false) / 2) +
635      kItemSpacing) / IconWidth(true);
636
637  // Because the user can drag outside the container bounds, we need to clamp to
638  // the valid range.  Note that the maximum allowable value is (num icons), not
639  // (num icons - 1), because we represent the indicator being past the last
640  // icon as being "before the (last + 1) icon".
641  int before_icon = std::min(std::max(before_icon_unclamped, 0),
642                             static_cast<int>(VisibleBrowserActions()));
643
644  // Now we convert back to a pixel offset into the container.  We want to place
645  // the center of the drop indicator at the midpoint of the space before our
646  // chosen icon.
647  SetDropIndicator(width_before_icons + (before_icon * IconWidth(true)) -
648      (kItemSpacing / 2));
649
650  return ui::DragDropTypes::DRAG_MOVE;
651}
652
653void BrowserActionsContainer::OnDragExited() {
654  StopShowFolderDropMenuTimer();
655  drop_indicator_position_ = -1;
656  SchedulePaint();
657}
658
659int BrowserActionsContainer::OnPerformDrop(
660    const views::DropTargetEvent& event) {
661  BrowserActionDragData data;
662  if (!data.Read(event.data()))
663    return ui::DragDropTypes::DRAG_NONE;
664
665  // Make sure we have the same view as we started with.
666  DCHECK_EQ(browser_action_views_[data.index()]->button()->extension()->id(),
667            data.id());
668  DCHECK(model_);
669
670  size_t i = 0;
671  for (; i < browser_action_views_.size(); ++i) {
672    int view_x = browser_action_views_[i]->GetMirroredBounds().x();
673    if (!browser_action_views_[i]->IsVisible() ||
674        (base::i18n::IsRTL() ? (view_x < drop_indicator_position_) :
675            (view_x >= drop_indicator_position_))) {
676      // We have reached the end of the visible icons or found one that has a
677      // higher x position than the drop point.
678      break;
679    }
680  }
681
682  // |i| now points to the item to the right of the drop indicator*, which is
683  // correct when dragging an icon to the left. When dragging to the right,
684  // however, we want the icon being dragged to get the index of the item to
685  // the left of the drop indicator, so we subtract one.
686  // * Well, it can also point to the end, but not when dragging to the left. :)
687  if (i > data.index())
688    --i;
689
690  if (profile_->IsOffTheRecord())
691    i = model_->IncognitoIndexToOriginal(i);
692
693  model_->MoveBrowserAction(
694      browser_action_views_[data.index()]->button()->extension(), i);
695
696  OnDragExited();  // Perform clean up after dragging.
697  return ui::DragDropTypes::DRAG_MOVE;
698}
699
700void BrowserActionsContainer::GetAccessibleState(
701    ui::AccessibleViewState* state) {
702  state->role = ui::AccessibilityTypes::ROLE_GROUPING;
703  state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_EXTENSIONS);
704}
705
706void BrowserActionsContainer::RunMenu(View* source, const gfx::Point& pt) {
707  if (source == chevron_) {
708    overflow_menu_ = new BrowserActionOverflowMenuController(
709        this, chevron_, browser_action_views_, VisibleBrowserActions());
710    overflow_menu_->set_observer(this);
711    overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), false);
712  }
713}
714
715void BrowserActionsContainer::WriteDragDataForView(View* sender,
716                                                   const gfx::Point& press_pt,
717                                                   OSExchangeData* data) {
718  DCHECK(data);
719
720  for (size_t i = 0; i < browser_action_views_.size(); ++i) {
721    BrowserActionButton* button = browser_action_views_[i]->button();
722    if (button == sender) {
723      // Set the dragging image for the icon.
724      scoped_ptr<gfx::Canvas> canvas(
725          browser_action_views_[i]->GetIconWithBadge());
726      drag_utils::SetDragImageOnDataObject(*canvas, button->size(), press_pt,
727                                           data);
728
729      // Fill in the remaining info.
730      BrowserActionDragData drag_data(
731          browser_action_views_[i]->button()->extension()->id(), i);
732      drag_data.Write(profile_, data);
733      break;
734    }
735  }
736}
737
738int BrowserActionsContainer::GetDragOperationsForView(View* sender,
739                                                      const gfx::Point& p) {
740  return ui::DragDropTypes::DRAG_MOVE;
741}
742
743bool BrowserActionsContainer::CanStartDragForView(View* sender,
744                                                  const gfx::Point& press_pt,
745                                                  const gfx::Point& p) {
746  return true;
747}
748
749void BrowserActionsContainer::OnResize(int resize_amount, bool done_resizing) {
750  if (!done_resizing) {
751    resize_amount_ = resize_amount;
752    OnBrowserActionVisibilityChanged();
753    return;
754  }
755
756  // Up until now we've only been modifying the resize_amount, but now it is
757  // time to set the container size to the size we have resized to, and then
758  // animate to the nearest icon count size if necessary (which may be 0).
759  int max_width = IconCountToWidth(-1, false);
760  container_width_ =
761      std::min(std::max(0, container_width_ - resize_amount), max_width);
762  SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT,
763                            WidthToIconCount(container_width_));
764}
765
766void BrowserActionsContainer::AnimationProgressed(
767    const ui::Animation* animation) {
768  DCHECK_EQ(resize_animation_.get(), animation);
769  resize_amount_ = static_cast<int>(resize_animation_->GetCurrentValue() *
770      (container_width_ - animation_target_size_));
771  OnBrowserActionVisibilityChanged();
772}
773
774void BrowserActionsContainer::AnimationEnded(const ui::Animation* animation) {
775  container_width_ = animation_target_size_;
776  animation_target_size_ = 0;
777  resize_amount_ = 0;
778  OnBrowserActionVisibilityChanged();
779  suppress_chevron_ = false;
780}
781
782void BrowserActionsContainer::NotifyMenuDeleted(
783    BrowserActionOverflowMenuController* controller) {
784  DCHECK(controller == overflow_menu_);
785  overflow_menu_ = NULL;
786}
787
788void BrowserActionsContainer::InspectPopup(ExtensionAction* action) {
789  OnBrowserActionExecuted(GetBrowserActionView(action)->button(), true);
790}
791
792void BrowserActionsContainer::ExtensionPopupIsClosing(ExtensionPopup* popup) {
793  // ExtensionPopup is ref-counted, so we don't need to delete it.
794  DCHECK_EQ(popup_, popup);
795  popup_ = NULL;
796  popup_button_->SetButtonNotPushed();
797  popup_button_ = NULL;
798}
799
800void BrowserActionsContainer::MoveBrowserAction(const std::string& extension_id,
801                                                size_t new_index) {
802  ExtensionService* service = profile_->GetExtensionService();
803  if (service) {
804    const Extension* extension = service->GetExtensionById(extension_id, false);
805    model_->MoveBrowserAction(extension, new_index);
806    SchedulePaint();
807  }
808}
809
810void BrowserActionsContainer::HidePopup() {
811  if (popup_)
812    popup_->Close();
813}
814
815void BrowserActionsContainer::TestExecuteBrowserAction(int index) {
816  BrowserActionButton* button = browser_action_views_[index]->button();
817  OnBrowserActionExecuted(button, false);
818}
819
820void BrowserActionsContainer::TestSetIconVisibilityCount(size_t icons) {
821  model_->SetVisibleIconCount(icons);
822  chevron_->SetVisible(icons < browser_action_views_.size());
823  container_width_ = IconCountToWidth(icons, chevron_->IsVisible());
824  Layout();
825  SchedulePaint();
826}
827
828void BrowserActionsContainer::OnPaint(gfx::Canvas* canvas) {
829  // TODO(sky/glen): Instead of using a drop indicator, animate the icons while
830  // dragging (like we do for tab dragging).
831  if (drop_indicator_position_ > -1) {
832    // The two-pixel width drop indicator.
833    static const int kDropIndicatorWidth = 2;
834    gfx::Rect indicator_bounds(
835        drop_indicator_position_ - (kDropIndicatorWidth / 2),
836        ToolbarView::kVertSpacing, kDropIndicatorWidth, IconHeight());
837
838    // Color of the drop indicator.
839    static const SkColor kDropIndicatorColor = SK_ColorBLACK;
840    canvas->FillRectInt(kDropIndicatorColor, indicator_bounds.x(),
841                        indicator_bounds.y(), indicator_bounds.width(),
842                        indicator_bounds.height());
843  }
844}
845
846void BrowserActionsContainer::OnThemeChanged() {
847  LoadImages();
848}
849
850void BrowserActionsContainer::ViewHierarchyChanged(bool is_add,
851                                                   views::View* parent,
852                                                   views::View* child) {
853  // No extensions (e.g., incognito).
854  if (!model_)
855    return;
856
857  if (is_add && child == this) {
858    // Initial toolbar button creation and placement in the widget hierarchy.
859    // We do this here instead of in the constructor because AddBrowserAction
860    // calls Layout on the Toolbar, which needs this object to be constructed
861    // before its Layout function is called.
862    CreateBrowserActionViews();
863  }
864}
865
866// static
867int BrowserActionsContainer::IconWidth(bool include_padding) {
868  static bool initialized = false;
869  static int icon_width = 0;
870  if (!initialized) {
871    initialized = true;
872    icon_width = ResourceBundle::GetSharedInstance().GetBitmapNamed(
873        IDR_BROWSER_ACTION)->width();
874  }
875  return icon_width + (include_padding ? kItemSpacing : 0);
876}
877
878// static
879int BrowserActionsContainer::IconHeight() {
880  static bool initialized = false;
881  static int icon_height = 0;
882  if (!initialized) {
883    initialized = true;
884    icon_height = ResourceBundle::GetSharedInstance().GetBitmapNamed(
885        IDR_BROWSER_ACTION)->height();
886  }
887  return icon_height;
888}
889
890void BrowserActionsContainer::BrowserActionAdded(const Extension* extension,
891                                                 int index) {
892#if defined(DEBUG)
893  for (size_t i = 0; i < browser_action_views_.size(); ++i) {
894    DCHECK(browser_action_views_[i]->button()->extension() != extension) <<
895           "Asked to add a browser action view for an extension that already "
896           "exists.";
897  }
898#endif
899  CloseOverflowMenu();
900
901  if (!ShouldDisplayBrowserAction(extension))
902    return;
903
904  size_t visible_actions = VisibleBrowserActions();
905
906  // Add the new browser action to the vector and the view hierarchy.
907  if (profile_->IsOffTheRecord())
908    index = model_->OriginalIndexToIncognito(index);
909  BrowserActionView* view = new BrowserActionView(extension, this);
910  browser_action_views_.insert(browser_action_views_.begin() + index, view);
911  AddChildViewAt(view, index);
912
913  // If we are still initializing the container, don't bother animating.
914  if (!model_->extensions_initialized())
915    return;
916
917  // Enlarge the container if it was already at maximum size and we're not in
918  // the middle of upgrading.
919  if ((model_->GetVisibleIconCount() < 0) &&
920      !profile_->GetExtensionService()->IsBeingUpgraded(extension)) {
921    suppress_chevron_ = true;
922    SaveDesiredSizeAndAnimate(ui::Tween::LINEAR, visible_actions + 1);
923  } else {
924    // Just redraw the (possibly modified) visible icon set.
925    OnBrowserActionVisibilityChanged();
926  }
927}
928
929void BrowserActionsContainer::BrowserActionRemoved(const Extension* extension) {
930  CloseOverflowMenu();
931
932  if (popup_ && popup_->host()->extension() == extension)
933    HidePopup();
934
935  size_t visible_actions = VisibleBrowserActions();
936  for (BrowserActionViews::iterator iter = browser_action_views_.begin();
937       iter != browser_action_views_.end(); ++iter) {
938    if ((*iter)->button()->extension() == extension) {
939      RemoveChildView(*iter);
940      delete *iter;
941      browser_action_views_.erase(iter);
942
943      // If the extension is being upgraded we don't want the bar to shrink
944      // because the icon is just going to get re-added to the same location.
945      if (profile_->GetExtensionService()->IsBeingUpgraded(extension))
946        return;
947
948      if (browser_action_views_.size() > visible_actions) {
949        // If we have more icons than we can show, then we must not be changing
950        // the container size (since we either removed an icon from the main
951        // area and one from the overflow list will have shifted in, or we
952        // removed an entry directly from the overflow list).
953        OnBrowserActionVisibilityChanged();
954      } else {
955        // Either we went from overflow to no-overflow, or we shrunk the no-
956        // overflow container by 1.  Either way the size changed, so animate.
957        chevron_->SetVisible(false);
958        SaveDesiredSizeAndAnimate(ui::Tween::EASE_OUT,
959                                  browser_action_views_.size());
960      }
961      return;
962    }
963  }
964}
965
966void BrowserActionsContainer::BrowserActionMoved(const Extension* extension,
967                                                 int index) {
968  if (!ShouldDisplayBrowserAction(extension))
969    return;
970
971  if (profile_->IsOffTheRecord())
972    index = model_->OriginalIndexToIncognito(index);
973
974  DCHECK(index >= 0 && index < static_cast<int>(browser_action_views_.size()));
975
976  DeleteBrowserActionViews();
977  CreateBrowserActionViews();
978  Layout();
979  SchedulePaint();
980}
981
982void BrowserActionsContainer::ModelLoaded() {
983  SetContainerWidth();
984}
985
986void BrowserActionsContainer::LoadImages() {
987  ui::ThemeProvider* tp = GetThemeProvider();
988  chevron_->SetIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW));
989  chevron_->SetHoverIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_H));
990  chevron_->SetPushedIcon(*tp->GetBitmapNamed(IDR_BROWSER_ACTIONS_OVERFLOW_P));
991}
992
993void BrowserActionsContainer::SetContainerWidth() {
994  int visible_actions = model_->GetVisibleIconCount();
995  if (visible_actions < 0)  // All icons should be visible.
996    visible_actions = model_->size();
997  chevron_->SetVisible(static_cast<size_t>(visible_actions) < model_->size());
998  container_width_ = IconCountToWidth(visible_actions, chevron_->IsVisible());
999}
1000
1001void BrowserActionsContainer::CloseOverflowMenu() {
1002  if (overflow_menu_)
1003    overflow_menu_->CancelMenu();
1004}
1005
1006void BrowserActionsContainer::StopShowFolderDropMenuTimer() {
1007  show_menu_task_factory_.RevokeAll();
1008}
1009
1010void BrowserActionsContainer::StartShowFolderDropMenuTimer() {
1011  int delay = views::GetMenuShowDelay();
1012  MessageLoop::current()->PostDelayedTask(FROM_HERE,
1013      show_menu_task_factory_.NewRunnableMethod(
1014          &BrowserActionsContainer::ShowDropFolder),
1015      delay);
1016}
1017
1018void BrowserActionsContainer::ShowDropFolder() {
1019  DCHECK(!overflow_menu_);
1020  SetDropIndicator(-1);
1021  overflow_menu_ = new BrowserActionOverflowMenuController(
1022      this, chevron_, browser_action_views_, VisibleBrowserActions());
1023  overflow_menu_->set_observer(this);
1024  overflow_menu_->RunMenu(GetWindow()->GetNativeWindow(), true);
1025}
1026
1027void BrowserActionsContainer::SetDropIndicator(int x_pos) {
1028  if (drop_indicator_position_ != x_pos) {
1029    drop_indicator_position_ = x_pos;
1030    SchedulePaint();
1031  }
1032}
1033
1034int BrowserActionsContainer::IconCountToWidth(int icons,
1035                                              bool display_chevron) const {
1036  if (icons < 0)
1037    icons = browser_action_views_.size();
1038  if ((icons == 0) && !display_chevron)
1039    return ToolbarView::kStandardSpacing;
1040  int icons_size =
1041      (icons == 0) ? 0 : ((icons * IconWidth(true)) - kItemSpacing);
1042  int chevron_size = display_chevron ?
1043      (kChevronSpacing + chevron_->GetPreferredSize().width()) : 0;
1044  return ToolbarView::kStandardSpacing + icons_size + chevron_size +
1045      ToolbarView::kStandardSpacing;
1046}
1047
1048size_t BrowserActionsContainer::WidthToIconCount(int pixels) const {
1049  // Check for widths large enough to show the entire icon set.
1050  if (pixels >= IconCountToWidth(-1, false))
1051    return browser_action_views_.size();
1052
1053  // We need to reserve space for the resize area, chevron, and the spacing on
1054  // either side of the chevron.
1055  int available_space = pixels - ToolbarView::kStandardSpacing -
1056      chevron_->GetPreferredSize().width() - kChevronSpacing -
1057      ToolbarView::kStandardSpacing;
1058  // Now we add an extra between-item padding value so the space can be divided
1059  // evenly by (size of icon with padding).
1060  return static_cast<size_t>(
1061      std::max(0, available_space + kItemSpacing) / IconWidth(true));
1062}
1063
1064int BrowserActionsContainer::ContainerMinSize() const {
1065  return ToolbarView::kStandardSpacing + kChevronSpacing +
1066      chevron_->GetPreferredSize().width() + ToolbarView::kStandardSpacing;
1067}
1068
1069void BrowserActionsContainer::SaveDesiredSizeAndAnimate(
1070    ui::Tween::Type tween_type,
1071    size_t num_visible_icons) {
1072  // Save off the desired number of visible icons.  We do this now instead of at
1073  // the end of the animation so that even if the browser is shut down while
1074  // animating, the right value will be restored on next run.
1075  // NOTE: Don't save the icon count in incognito because there may be fewer
1076  // icons in that mode. The result is that the container in a normal window is
1077  // always at least as wide as in an incognito window.
1078  if (!profile_->IsOffTheRecord())
1079    model_->SetVisibleIconCount(num_visible_icons);
1080
1081  int target_size = IconCountToWidth(num_visible_icons,
1082      num_visible_icons < browser_action_views_.size());
1083  if (!disable_animations_during_testing_) {
1084    // Animate! We have to set the animation_target_size_ after calling Reset(),
1085    // because that could end up calling AnimationEnded which clears the value.
1086    resize_animation_->Reset();
1087    resize_animation_->SetTweenType(tween_type);
1088    animation_target_size_ = target_size;
1089    resize_animation_->Show();
1090  } else {
1091    animation_target_size_ = target_size;
1092    AnimationEnded(resize_animation_.get());
1093  }
1094}
1095
1096bool BrowserActionsContainer::ShouldDisplayBrowserAction(
1097    const Extension* extension) {
1098  // Only display incognito-enabled extensions while in incognito mode.
1099  return
1100      (!profile_->IsOffTheRecord() ||
1101       profile_->GetExtensionService()->IsIncognitoEnabled(extension->id()));
1102}
1103