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/gtk/browser_actions_toolbar_gtk.h"
6
7#include <algorithm>
8#include <vector>
9
10#include "base/i18n/rtl.h"
11#include "base/utf_string_conversions.h"
12#include "chrome/browser/extensions/extension_browser_event_router.h"
13#include "chrome/browser/extensions/extension_context_menu_model.h"
14#include "chrome/browser/extensions/extension_service.h"
15#include "chrome/browser/extensions/image_loading_tracker.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/ui/browser.h"
18#include "chrome/browser/ui/gtk/cairo_cached_surface.h"
19#include "chrome/browser/ui/gtk/extensions/extension_popup_gtk.h"
20#include "chrome/browser/ui/gtk/gtk_chrome_button.h"
21#include "chrome/browser/ui/gtk/gtk_chrome_shrinkable_hbox.h"
22#include "chrome/browser/ui/gtk/gtk_theme_service.h"
23#include "chrome/browser/ui/gtk/gtk_util.h"
24#include "chrome/browser/ui/gtk/hover_controller_gtk.h"
25#include "chrome/browser/ui/gtk/menu_gtk.h"
26#include "chrome/browser/ui/gtk/view_id_util.h"
27#include "chrome/common/extensions/extension.h"
28#include "chrome/common/extensions/extension_action.h"
29#include "chrome/common/extensions/extension_resource.h"
30#include "content/browser/tab_contents/tab_contents.h"
31#include "content/common/notification_details.h"
32#include "content/common/notification_service.h"
33#include "content/common/notification_source.h"
34#include "content/common/notification_type.h"
35#include "grit/app_resources.h"
36#include "grit/theme_resources.h"
37#include "ui/gfx/canvas_skia_paint.h"
38#include "ui/gfx/gtk_util.h"
39
40namespace {
41
42// The width of the browser action buttons.
43const int kButtonWidth = 27;
44
45// The padding between browser action buttons.
46const int kButtonPadding = 4;
47
48// The padding to the right of the browser action buttons (between the buttons
49// and chevron if they are both showing).
50const int kButtonChevronPadding = 2;
51
52// The padding to the left, top and bottom of the browser actions toolbar
53// separator.
54const int kSeparatorPadding = 2;
55
56// Width of the invisible gripper for resizing the toolbar.
57const int kResizeGripperWidth = 4;
58
59const char* kDragTarget = "application/x-chrome-browseraction";
60
61GtkTargetEntry GetDragTargetEntry() {
62  static std::string drag_target_string(kDragTarget);
63  GtkTargetEntry drag_target;
64  drag_target.target = const_cast<char*>(drag_target_string.c_str());
65  drag_target.flags = GTK_TARGET_SAME_APP;
66  drag_target.info = 0;
67  return drag_target;
68}
69
70// The minimum width in pixels of the button hbox if |icon_count| icons are
71// showing.
72gint WidthForIconCount(gint icon_count) {
73  return std::max((kButtonWidth + kButtonPadding) * icon_count - kButtonPadding,
74                  0);
75}
76
77}  // namespace
78
79using ui::SimpleMenuModel;
80
81class BrowserActionButton : public NotificationObserver,
82                            public ImageLoadingTracker::Observer,
83                            public ExtensionContextMenuModel::PopupDelegate,
84                            public MenuGtk::Delegate {
85 public:
86  BrowserActionButton(BrowserActionsToolbarGtk* toolbar,
87                      const Extension* extension,
88                      GtkThemeService* theme_provider)
89      : toolbar_(toolbar),
90        extension_(extension),
91        image_(NULL),
92        tracker_(this),
93        tab_specific_icon_(NULL),
94        default_icon_(NULL) {
95    button_.reset(new CustomDrawButton(
96        theme_provider,
97        IDR_BROWSER_ACTION,
98        IDR_BROWSER_ACTION_P,
99        IDR_BROWSER_ACTION_H,
100        0,
101        NULL));
102    alignment_.Own(gtk_alignment_new(0, 0, 1, 1));
103    gtk_container_add(GTK_CONTAINER(alignment_.get()), button());
104    gtk_widget_show(button());
105
106    DCHECK(extension_->browser_action());
107
108    UpdateState();
109
110    // The Browser Action API does not allow the default icon path to be
111    // changed at runtime, so we can load this now and cache it.
112    std::string path = extension_->browser_action()->default_icon_path();
113    if (!path.empty()) {
114      tracker_.LoadImage(extension_, extension_->GetResource(path),
115                         gfx::Size(Extension::kBrowserActionIconMaxSize,
116                                   Extension::kBrowserActionIconMaxSize),
117                         ImageLoadingTracker::DONT_CACHE);
118    }
119
120    signals_.Connect(button(), "button-press-event",
121                     G_CALLBACK(OnButtonPress), this);
122    signals_.Connect(button(), "clicked",
123                     G_CALLBACK(OnClicked), this);
124    signals_.Connect(button(), "drag-begin",
125                     G_CALLBACK(&OnDragBegin), this);
126    signals_.ConnectAfter(widget(), "expose-event",
127                          G_CALLBACK(OnExposeEvent), this);
128
129    registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED,
130                   Source<ExtensionAction>(extension->browser_action()));
131  }
132
133  ~BrowserActionButton() {
134    if (tab_specific_icon_)
135      g_object_unref(tab_specific_icon_);
136
137    if (default_icon_)
138      g_object_unref(default_icon_);
139
140    alignment_.Destroy();
141  }
142
143  GtkWidget* button() { return button_->widget(); }
144
145  GtkWidget* widget() { return alignment_.get(); }
146
147  const Extension* extension() { return extension_; }
148
149  // NotificationObserver implementation.
150  void Observe(NotificationType type,
151               const NotificationSource& source,
152               const NotificationDetails& details) {
153    if (type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED)
154      UpdateState();
155    else
156      NOTREACHED();
157  }
158
159  // ImageLoadingTracker::Observer implementation.
160  void OnImageLoaded(SkBitmap* image, const ExtensionResource& resource,
161                     int index) {
162    if (image) {
163      default_skbitmap_ = *image;
164      default_icon_ = gfx::GdkPixbufFromSkBitmap(image);
165    }
166    UpdateState();
167  }
168
169  // Updates the button based on the latest state from the associated
170  // browser action.
171  void UpdateState() {
172    int tab_id = toolbar_->GetCurrentTabId();
173    if (tab_id < 0)
174      return;
175
176    std::string tooltip = extension_->browser_action()->GetTitle(tab_id);
177    if (tooltip.empty())
178      gtk_widget_set_has_tooltip(button(), FALSE);
179    else
180      gtk_widget_set_tooltip_text(button(), tooltip.c_str());
181
182    SkBitmap image = extension_->browser_action()->GetIcon(tab_id);
183    if (!image.isNull()) {
184      GdkPixbuf* previous_gdk_icon = tab_specific_icon_;
185      tab_specific_icon_ = gfx::GdkPixbufFromSkBitmap(&image);
186      SetImage(tab_specific_icon_);
187      if (previous_gdk_icon)
188        g_object_unref(previous_gdk_icon);
189    } else if (default_icon_) {
190      SetImage(default_icon_);
191    }
192    gtk_widget_queue_draw(button());
193  }
194
195  SkBitmap GetIcon() {
196    const SkBitmap& image = extension_->browser_action()->GetIcon(
197        toolbar_->GetCurrentTabId());
198    if (!image.isNull()) {
199      return image;
200    } else {
201      return default_skbitmap_;
202    }
203  }
204
205  MenuGtk* GetContextMenu() {
206    if (!extension_->ShowConfigureContextMenus())
207      return NULL;
208
209    context_menu_model_ =
210        new ExtensionContextMenuModel(extension_, toolbar_->browser(), this);
211    context_menu_.reset(
212        new MenuGtk(this, context_menu_model_.get()));
213    return context_menu_.get();
214  }
215
216 private:
217  // MenuGtk::Delegate implementation.
218  virtual void StoppedShowing() {
219    button_->UnsetPaintOverride();
220
221    // If the context menu was showing for the overflow menu, re-assert the
222    // grab that was shadowed.
223    if (toolbar_->overflow_menu_.get())
224      gtk_util::GrabAllInput(toolbar_->overflow_menu_->widget());
225  }
226
227  virtual void CommandWillBeExecuted() {
228    // If the context menu was showing for the overflow menu, and a command
229    // is executed, then stop showing the overflow menu.
230    if (toolbar_->overflow_menu_.get())
231      toolbar_->overflow_menu_->Cancel();
232  }
233
234  // Returns true to prevent further processing of the event that caused us to
235  // show the popup, or false to continue processing.
236  bool ShowPopup(bool devtools) {
237    ExtensionAction* browser_action = extension_->browser_action();
238
239    int tab_id = toolbar_->GetCurrentTabId();
240    if (tab_id < 0) {
241      NOTREACHED() << "No current tab.";
242      return true;
243    }
244
245    if (browser_action->HasPopup(tab_id)) {
246      ExtensionPopupGtk::Show(
247          browser_action->GetPopupUrl(tab_id), toolbar_->browser(),
248          widget(), devtools);
249      return true;
250    }
251
252    return false;
253  }
254
255  // ExtensionContextMenuModel::PopupDelegate implementation.
256  virtual void InspectPopup(ExtensionAction* action) {
257    ShowPopup(true);
258  }
259
260  void SetImage(GdkPixbuf* image) {
261    if (!image_) {
262      image_ = gtk_image_new_from_pixbuf(image);
263      gtk_button_set_image(GTK_BUTTON(button()), image_);
264    } else {
265      gtk_image_set_from_pixbuf(GTK_IMAGE(image_), image);
266    }
267  }
268
269  static gboolean OnButtonPress(GtkWidget* widget,
270                                GdkEventButton* event,
271                                BrowserActionButton* action) {
272    if (event->button != 3)
273      return FALSE;
274
275    MenuGtk* menu = action->GetContextMenu();
276    if (!menu)
277      return FALSE;
278
279    action->button_->SetPaintOverride(GTK_STATE_ACTIVE);
280    menu->PopupForWidget(widget, event->button, event->time);
281
282    return TRUE;
283  }
284
285  static void OnClicked(GtkWidget* widget, BrowserActionButton* action) {
286    if (action->ShowPopup(false))
287      return;
288
289    ExtensionService* service =
290        action->toolbar_->browser()->profile()->GetExtensionService();
291    service->browser_event_router()->BrowserActionExecuted(
292        action->toolbar_->browser()->profile(), action->extension_->id(),
293        action->toolbar_->browser());
294  }
295
296  static gboolean OnExposeEvent(GtkWidget* widget,
297                                GdkEventExpose* event,
298                                BrowserActionButton* button) {
299    int tab_id = button->toolbar_->GetCurrentTabId();
300    if (tab_id < 0)
301      return FALSE;
302
303    ExtensionAction* action = button->extension_->browser_action();
304    if (action->GetBadgeText(tab_id).empty())
305      return FALSE;
306
307    gfx::CanvasSkiaPaint canvas(event, false);
308    gfx::Rect bounding_rect(widget->allocation);
309    action->PaintBadge(&canvas, bounding_rect, tab_id);
310    return FALSE;
311  }
312
313  static void OnDragBegin(GtkWidget* widget,
314                          GdkDragContext* drag_context,
315                          BrowserActionButton* button) {
316    // Simply pass along the notification to the toolbar. The point of this
317    // function is to tell the toolbar which BrowserActionButton initiated the
318    // drag.
319    button->toolbar_->DragStarted(button, drag_context);
320  }
321
322  // The toolbar containing this button.
323  BrowserActionsToolbarGtk* toolbar_;
324
325  // The extension that contains this browser action.
326  const Extension* extension_;
327
328  // The button for this browser action.
329  scoped_ptr<CustomDrawButton> button_;
330
331  // The top level widget (parent of |button_|).
332  OwnedWidgetGtk alignment_;
333
334  // The one image subwidget in |button_|. We keep this out so we don't alter
335  // the widget hierarchy while changing the button image because changing the
336  // GTK widget hierarchy invalidates all tooltips and several popular
337  // extensions change browser action icon in a loop.
338  GtkWidget* image_;
339
340  // Loads the button's icons for us on the file thread.
341  ImageLoadingTracker tracker_;
342
343  // If we are displaying a tab-specific icon, it will be here.
344  GdkPixbuf* tab_specific_icon_;
345
346  // If the browser action has a default icon, it will be here.
347  GdkPixbuf* default_icon_;
348
349  // Same as |default_icon_|, but stored as SkBitmap.
350  SkBitmap default_skbitmap_;
351
352  ui::GtkSignalRegistrar signals_;
353  NotificationRegistrar registrar_;
354
355  // The context menu view and model for this extension action.
356  scoped_ptr<MenuGtk> context_menu_;
357  scoped_refptr<ExtensionContextMenuModel> context_menu_model_;
358
359  friend class BrowserActionsToolbarGtk;
360};
361
362// BrowserActionsToolbarGtk ----------------------------------------------------
363
364BrowserActionsToolbarGtk::BrowserActionsToolbarGtk(Browser* browser)
365    : browser_(browser),
366      profile_(browser->profile()),
367      theme_service_(GtkThemeService::GetFrom(browser->profile())),
368      model_(NULL),
369      hbox_(gtk_hbox_new(FALSE, 0)),
370      button_hbox_(gtk_chrome_shrinkable_hbox_new(TRUE, FALSE, kButtonPadding)),
371      drag_button_(NULL),
372      drop_index_(-1),
373      resize_animation_(this),
374      desired_width_(0),
375      start_width_(0),
376      method_factory_(this) {
377  ExtensionService* extension_service = profile_->GetExtensionService();
378  // The |extension_service| can be NULL in Incognito.
379  if (!extension_service)
380    return;
381
382  overflow_button_.reset(new CustomDrawButton(
383      theme_service_,
384      IDR_BROWSER_ACTIONS_OVERFLOW,
385      IDR_BROWSER_ACTIONS_OVERFLOW_P,
386      IDR_BROWSER_ACTIONS_OVERFLOW_H,
387      0,
388      gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE)));
389
390  GtkWidget* gripper = gtk_button_new();
391  gtk_widget_set_size_request(gripper, kResizeGripperWidth, -1);
392  GTK_WIDGET_UNSET_FLAGS(gripper, GTK_CAN_FOCUS);
393  gtk_widget_add_events(gripper, GDK_POINTER_MOTION_MASK);
394  signals_.Connect(gripper, "motion-notify-event",
395                   G_CALLBACK(OnGripperMotionNotifyThunk), this);
396  signals_.Connect(gripper, "expose-event",
397                   G_CALLBACK(OnGripperExposeThunk), this);
398  signals_.Connect(gripper, "enter-notify-event",
399                   G_CALLBACK(OnGripperEnterNotifyThunk), this);
400  signals_.Connect(gripper, "leave-notify-event",
401                   G_CALLBACK(OnGripperLeaveNotifyThunk), this);
402  signals_.Connect(gripper, "button-release-event",
403                   G_CALLBACK(OnGripperButtonReleaseThunk), this);
404  signals_.Connect(gripper, "button-press-event",
405                   G_CALLBACK(OnGripperButtonPressThunk), this);
406  signals_.Connect(chevron(), "button-press-event",
407                   G_CALLBACK(OnOverflowButtonPressThunk), this);
408
409  // |overflow_alignment| adds padding to the right of the browser action
410  // buttons, but only appears when the overflow menu is showing.
411  overflow_alignment_ = gtk_alignment_new(0, 0, 1, 1);
412  gtk_container_add(GTK_CONTAINER(overflow_alignment_), chevron());
413
414  // |overflow_area_| holds the overflow chevron and the separator, which
415  // is only shown in GTK+ theme mode.
416  overflow_area_ = gtk_hbox_new(FALSE, 0);
417  gtk_box_pack_start(GTK_BOX(overflow_area_), overflow_alignment_,
418                     FALSE, FALSE, 0);
419
420  separator_ = gtk_vseparator_new();
421  gtk_box_pack_start(GTK_BOX(overflow_area_), separator_,
422                     FALSE, FALSE, 0);
423  gtk_widget_set_no_show_all(separator_, TRUE);
424
425  gtk_widget_show_all(overflow_area_);
426  gtk_widget_set_no_show_all(overflow_area_, TRUE);
427
428  gtk_box_pack_start(GTK_BOX(hbox_.get()), gripper, FALSE, FALSE, 0);
429  gtk_box_pack_start(GTK_BOX(hbox_.get()), button_hbox_.get(), TRUE, TRUE, 0);
430  gtk_box_pack_start(GTK_BOX(hbox_.get()), overflow_area_, FALSE, FALSE, 0);
431
432  model_ = extension_service->toolbar_model();
433  model_->AddObserver(this);
434  SetupDrags();
435
436  if (model_->extensions_initialized()) {
437    CreateAllButtons();
438    SetContainerWidth();
439  }
440
441  // We want to connect to "set-focus" on the toplevel window; we have to wait
442  // until we are added to a toplevel window to do so.
443  signals_.Connect(widget(), "hierarchy-changed",
444                   G_CALLBACK(OnHierarchyChangedThunk), this);
445
446  ViewIDUtil::SetID(button_hbox_.get(), VIEW_ID_BROWSER_ACTION_TOOLBAR);
447
448  registrar_.Add(this,
449                 NotificationType::BROWSER_THEME_CHANGED,
450                 NotificationService::AllSources());
451  theme_service_->InitThemesFor(this);
452}
453
454BrowserActionsToolbarGtk::~BrowserActionsToolbarGtk() {
455  if (model_)
456    model_->RemoveObserver(this);
457  button_hbox_.Destroy();
458  hbox_.Destroy();
459}
460
461int BrowserActionsToolbarGtk::GetCurrentTabId() {
462  TabContents* selected_tab = browser_->GetSelectedTabContents();
463  if (!selected_tab)
464    return -1;
465
466  return selected_tab->controller().session_id().id();
467}
468
469void BrowserActionsToolbarGtk::Update() {
470  for (ExtensionButtonMap::iterator iter = extension_button_map_.begin();
471       iter != extension_button_map_.end(); ++iter) {
472    iter->second->UpdateState();
473  }
474}
475
476void BrowserActionsToolbarGtk::Observe(NotificationType type,
477                                       const NotificationSource& source,
478                                       const NotificationDetails& details) {
479  DCHECK(NotificationType::BROWSER_THEME_CHANGED == type);
480  if (theme_service_->UseGtkTheme())
481    gtk_widget_show(separator_);
482  else
483    gtk_widget_hide(separator_);
484}
485
486void BrowserActionsToolbarGtk::SetupDrags() {
487  GtkTargetEntry drag_target = GetDragTargetEntry();
488  gtk_drag_dest_set(button_hbox_.get(), GTK_DEST_DEFAULT_DROP, &drag_target, 1,
489                    GDK_ACTION_MOVE);
490
491  signals_.Connect(button_hbox_.get(), "drag-motion",
492                   G_CALLBACK(OnDragMotionThunk), this);
493}
494
495void BrowserActionsToolbarGtk::CreateAllButtons() {
496  extension_button_map_.clear();
497
498  int i = 0;
499  for (ExtensionList::iterator iter = model_->begin();
500       iter != model_->end(); ++iter) {
501    CreateButtonForExtension(*iter, i++);
502  }
503}
504
505void BrowserActionsToolbarGtk::SetContainerWidth() {
506  int showing_actions = model_->GetVisibleIconCount();
507  if (showing_actions >= 0)
508    SetButtonHBoxWidth(WidthForIconCount(showing_actions));
509}
510
511void BrowserActionsToolbarGtk::CreateButtonForExtension(
512    const Extension* extension, int index) {
513  if (!ShouldDisplayBrowserAction(extension))
514    return;
515
516  if (profile_->IsOffTheRecord())
517    index = model_->OriginalIndexToIncognito(index);
518
519  RemoveButtonForExtension(extension);
520  linked_ptr<BrowserActionButton> button(
521      new BrowserActionButton(this, extension, theme_service_));
522  gtk_chrome_shrinkable_hbox_pack_start(
523      GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()), button->widget(), 0);
524  gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button->widget(), index);
525  extension_button_map_[extension->id()] = button;
526
527  GtkTargetEntry drag_target = GetDragTargetEntry();
528  gtk_drag_source_set(button->button(), GDK_BUTTON1_MASK, &drag_target, 1,
529                      GDK_ACTION_MOVE);
530  // We ignore whether the drag was a "success" or "failure" in Gtk's opinion.
531  signals_.Connect(button->button(), "drag-end",
532                   G_CALLBACK(&OnDragEndThunk), this);
533  signals_.Connect(button->button(), "drag-failed",
534                   G_CALLBACK(&OnDragFailedThunk), this);
535
536  // Any time a browser action button is shown or hidden we have to update
537  // the chevron state.
538  signals_.Connect(button->widget(), "show",
539                   G_CALLBACK(&OnButtonShowOrHideThunk), this);
540  signals_.Connect(button->widget(), "hide",
541                   G_CALLBACK(&OnButtonShowOrHideThunk), this);
542
543  gtk_widget_show(button->widget());
544
545  UpdateVisibility();
546}
547
548GtkWidget* BrowserActionsToolbarGtk::GetBrowserActionWidget(
549    const Extension* extension) {
550  ExtensionButtonMap::iterator it = extension_button_map_.find(
551      extension->id());
552  if (it == extension_button_map_.end())
553    return NULL;
554
555  return it->second.get()->widget();
556}
557
558void BrowserActionsToolbarGtk::RemoveButtonForExtension(
559    const Extension* extension) {
560  if (extension_button_map_.erase(extension->id()))
561    UpdateVisibility();
562  UpdateChevronVisibility();
563}
564
565void BrowserActionsToolbarGtk::UpdateVisibility() {
566  if (button_count() == 0)
567    gtk_widget_hide(widget());
568  else
569    gtk_widget_show(widget());
570}
571
572bool BrowserActionsToolbarGtk::ShouldDisplayBrowserAction(
573    const Extension* extension) {
574  // Only display incognito-enabled extensions while in incognito mode.
575  return (!profile_->IsOffTheRecord() ||
576          profile_->GetExtensionService()->IsIncognitoEnabled(extension->id()));
577}
578
579void BrowserActionsToolbarGtk::HidePopup() {
580  ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup();
581  if (popup)
582    popup->DestroyPopup();
583}
584
585void BrowserActionsToolbarGtk::AnimateToShowNIcons(int count) {
586  desired_width_ = WidthForIconCount(count);
587  start_width_ = button_hbox_->allocation.width;
588  resize_animation_.Reset();
589  resize_animation_.Show();
590}
591
592void BrowserActionsToolbarGtk::BrowserActionAdded(const Extension* extension,
593                                                  int index) {
594  overflow_menu_.reset();
595
596  CreateButtonForExtension(extension, index);
597
598  // If we are still initializing the container, don't bother animating.
599  if (!model_->extensions_initialized())
600    return;
601
602  // Animate the addition if we are showing all browser action buttons.
603  if (!GTK_WIDGET_VISIBLE(overflow_area_)) {
604    AnimateToShowNIcons(button_count());
605    model_->SetVisibleIconCount(button_count());
606  }
607}
608
609void BrowserActionsToolbarGtk::BrowserActionRemoved(
610    const Extension* extension) {
611  overflow_menu_.reset();
612
613  if (drag_button_ != NULL) {
614    // Break the current drag.
615    gtk_grab_remove(button_hbox_.get());
616  }
617
618  RemoveButtonForExtension(extension);
619
620  if (!GTK_WIDGET_VISIBLE(overflow_area_)) {
621    AnimateToShowNIcons(button_count());
622    model_->SetVisibleIconCount(button_count());
623  }
624}
625
626void BrowserActionsToolbarGtk::BrowserActionMoved(const Extension* extension,
627                                                  int index) {
628  // We initiated this move action, and have already moved the button.
629  if (drag_button_ != NULL)
630    return;
631
632  GtkWidget* button_widget = GetBrowserActionWidget(extension);
633  if (!button_widget) {
634    if (ShouldDisplayBrowserAction(extension))
635      NOTREACHED();
636    return;
637  }
638
639  if (profile_->IsOffTheRecord())
640    index = model_->OriginalIndexToIncognito(index);
641
642  gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button_widget, index);
643}
644
645void BrowserActionsToolbarGtk::ModelLoaded() {
646  SetContainerWidth();
647}
648
649void BrowserActionsToolbarGtk::AnimationProgressed(
650    const ui::Animation* animation) {
651  int width = start_width_ + (desired_width_ - start_width_) *
652      animation->GetCurrentValue();
653  gtk_widget_set_size_request(button_hbox_.get(), width, -1);
654
655  if (width == desired_width_)
656    resize_animation_.Reset();
657}
658
659void BrowserActionsToolbarGtk::AnimationEnded(const ui::Animation* animation) {
660  gtk_widget_set_size_request(button_hbox_.get(), desired_width_, -1);
661  UpdateChevronVisibility();
662}
663
664bool BrowserActionsToolbarGtk::IsCommandIdChecked(int command_id) const {
665  return false;
666}
667
668bool BrowserActionsToolbarGtk::IsCommandIdEnabled(int command_id) const {
669  return true;
670}
671
672bool BrowserActionsToolbarGtk::GetAcceleratorForCommandId(
673    int command_id,
674    ui::Accelerator* accelerator) {
675  return false;
676}
677
678void BrowserActionsToolbarGtk::ExecuteCommand(int command_id) {
679  const Extension* extension = model_->GetExtensionByIndex(command_id);
680  ExtensionAction* browser_action = extension->browser_action();
681
682  int tab_id = GetCurrentTabId();
683  if (tab_id < 0) {
684    NOTREACHED() << "No current tab.";
685    return;
686  }
687
688  if (browser_action->HasPopup(tab_id)) {
689    ExtensionPopupGtk::Show(
690        browser_action->GetPopupUrl(tab_id), browser(),
691        chevron(),
692        false);
693  } else {
694    ExtensionService* service = browser()->profile()->GetExtensionService();
695    service->browser_event_router()->BrowserActionExecuted(
696        browser()->profile(), extension->id(), browser());
697  }
698}
699
700void BrowserActionsToolbarGtk::StoppedShowing() {
701  overflow_button_->UnsetPaintOverride();
702}
703
704bool BrowserActionsToolbarGtk::AlwaysShowIconForCmd(int command_id) const {
705  return true;
706}
707
708void BrowserActionsToolbarGtk::DragStarted(BrowserActionButton* button,
709                                           GdkDragContext* drag_context) {
710  // No representation of the widget following the cursor.
711  GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1);
712  gtk_drag_set_icon_pixbuf(drag_context, pixbuf, 0, 0);
713  g_object_unref(pixbuf);
714
715  DCHECK(!drag_button_);
716  drag_button_ = button;
717}
718
719void BrowserActionsToolbarGtk::SetButtonHBoxWidth(int new_width) {
720  gint max_width = WidthForIconCount(button_count());
721  new_width = std::min(max_width, new_width);
722  new_width = std::max(new_width, 0);
723  gtk_widget_set_size_request(button_hbox_.get(), new_width, -1);
724}
725
726void BrowserActionsToolbarGtk::UpdateChevronVisibility() {
727  int showing_icon_count =
728      gtk_chrome_shrinkable_hbox_get_visible_child_count(
729          GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()));
730  if (showing_icon_count == 0) {
731    gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_), 0, 0, 0, 0);
732  } else {
733    gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_), 0, 0,
734                              kButtonChevronPadding, 0);
735  }
736
737  if (button_count() > showing_icon_count) {
738    if (!GTK_WIDGET_VISIBLE(overflow_area_)) {
739      if (drag_button_) {
740        // During drags, when the overflow chevron shows for the first time,
741        // take that much space away from |button_hbox_| to make the drag look
742        // smoother.
743        GtkRequisition req;
744        gtk_widget_size_request(chevron(), &req);
745        gint overflow_width = req.width;
746        gtk_widget_size_request(button_hbox_.get(), &req);
747        gint button_hbox_width = req.width;
748        button_hbox_width = std::max(button_hbox_width - overflow_width, 0);
749        gtk_widget_set_size_request(button_hbox_.get(), button_hbox_width, -1);
750      }
751
752      gtk_widget_show(overflow_area_);
753    }
754  } else {
755    gtk_widget_hide(overflow_area_);
756  }
757}
758
759gboolean BrowserActionsToolbarGtk::OnDragMotion(GtkWidget* widget,
760                                                GdkDragContext* drag_context,
761                                                gint x, gint y, guint time) {
762  // Only handle drags we initiated.
763  if (!drag_button_)
764    return FALSE;
765
766  if (base::i18n::IsRTL())
767    x = widget->allocation.width - x;
768  drop_index_ = x < kButtonWidth ? 0 : x / (kButtonWidth + kButtonPadding);
769
770  // We will go ahead and reorder the child in order to provide visual feedback
771  // to the user. We don't inform the model that it has moved until the drag
772  // ends.
773  gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), drag_button_->widget(),
774                        drop_index_);
775
776  gdk_drag_status(drag_context, GDK_ACTION_MOVE, time);
777  return TRUE;
778}
779
780void BrowserActionsToolbarGtk::OnDragEnd(GtkWidget* button,
781                                         GdkDragContext* drag_context) {
782  if (drop_index_ != -1) {
783    if (profile_->IsOffTheRecord())
784      drop_index_ = model_->IncognitoIndexToOriginal(drop_index_);
785
786    model_->MoveBrowserAction(drag_button_->extension(), drop_index_);
787  }
788
789  drag_button_ = NULL;
790  drop_index_ = -1;
791}
792
793gboolean BrowserActionsToolbarGtk::OnDragFailed(GtkWidget* widget,
794                                                GdkDragContext* drag_context,
795                                                GtkDragResult result) {
796  // We connect to this signal and return TRUE so that the default failure
797  // animation (wherein the drag widget floats back to the start of the drag)
798  // does not show, and the drag-end signal is emitted immediately instead of
799  // several seconds later.
800  return TRUE;
801}
802
803void BrowserActionsToolbarGtk::OnHierarchyChanged(
804    GtkWidget* widget, GtkWidget* previous_toplevel) {
805  GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
806  if (!GTK_WIDGET_TOPLEVEL(toplevel))
807    return;
808
809  signals_.Connect(toplevel, "set-focus", G_CALLBACK(OnSetFocusThunk), this);
810}
811
812void BrowserActionsToolbarGtk::OnSetFocus(GtkWidget* widget,
813                                          GtkWidget* focus_widget) {
814  ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup();
815  // The focus of the parent window has changed. Close the popup. Delay the hide
816  // because it will destroy the RenderViewHost, which may still be on the
817  // call stack.
818  if (!popup || popup->being_inspected())
819    return;
820  MessageLoop::current()->PostTask(FROM_HERE,
821      method_factory_.NewRunnableMethod(&BrowserActionsToolbarGtk::HidePopup));
822}
823
824gboolean BrowserActionsToolbarGtk::OnGripperMotionNotify(
825    GtkWidget* widget, GdkEventMotion* event) {
826  if (!(event->state & GDK_BUTTON1_MASK))
827    return FALSE;
828
829  // Calculate how much the user dragged the gripper and subtract that off the
830  // button container's width.
831  int distance_dragged = base::i18n::IsRTL() ?
832      -event->x :
833      event->x - widget->allocation.width;
834  gint new_width = button_hbox_->allocation.width - distance_dragged;
835  SetButtonHBoxWidth(new_width);
836
837  return FALSE;
838}
839
840gboolean BrowserActionsToolbarGtk::OnGripperExpose(GtkWidget* gripper,
841                                                   GdkEventExpose* expose) {
842  return TRUE;
843}
844
845// These three signal handlers (EnterNotify, LeaveNotify, and ButtonRelease)
846// are used to give the gripper the resize cursor. Since it doesn't have its
847// own window, we have to set the cursor whenever the pointer moves into the
848// button or leaves the button, and be sure to leave it on when the user is
849// dragging.
850gboolean BrowserActionsToolbarGtk::OnGripperEnterNotify(
851    GtkWidget* gripper, GdkEventCrossing* event) {
852  gdk_window_set_cursor(gripper->window,
853                        gfx::GetCursor(GDK_SB_H_DOUBLE_ARROW));
854  return FALSE;
855}
856
857gboolean BrowserActionsToolbarGtk::OnGripperLeaveNotify(
858    GtkWidget* gripper, GdkEventCrossing* event) {
859  if (!(event->state & GDK_BUTTON1_MASK))
860    gdk_window_set_cursor(gripper->window, NULL);
861  return FALSE;
862}
863
864gboolean BrowserActionsToolbarGtk::OnGripperButtonRelease(
865    GtkWidget* gripper, GdkEventButton* event) {
866  gfx::Rect gripper_rect(0, 0,
867                         gripper->allocation.width, gripper->allocation.height);
868  gfx::Point release_point(event->x, event->y);
869  if (!gripper_rect.Contains(release_point))
870    gdk_window_set_cursor(gripper->window, NULL);
871
872  // After the user resizes the toolbar, we want to smartly resize it to be
873  // the perfect size to fit the buttons.
874  int visible_icon_count =
875      gtk_chrome_shrinkable_hbox_get_visible_child_count(
876          GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()));
877  AnimateToShowNIcons(visible_icon_count);
878  model_->SetVisibleIconCount(visible_icon_count);
879
880  return FALSE;
881}
882
883gboolean BrowserActionsToolbarGtk::OnGripperButtonPress(
884    GtkWidget* gripper, GdkEventButton* event) {
885  resize_animation_.Reset();
886
887  return FALSE;
888}
889
890gboolean BrowserActionsToolbarGtk::OnOverflowButtonPress(
891    GtkWidget* overflow, GdkEventButton* event) {
892  overflow_menu_model_.reset(new SimpleMenuModel(this));
893
894  int visible_icon_count =
895      gtk_chrome_shrinkable_hbox_get_visible_child_count(
896          GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()));
897  for (int i = visible_icon_count; i < button_count(); ++i) {
898    int model_index = i;
899    if (profile_->IsOffTheRecord())
900      model_index = model_->IncognitoIndexToOriginal(i);
901
902    const Extension* extension = model_->GetExtensionByIndex(model_index);
903    BrowserActionButton* button = extension_button_map_[extension->id()].get();
904
905    overflow_menu_model_->AddItem(model_index, UTF8ToUTF16(extension->name()));
906    overflow_menu_model_->SetIcon(overflow_menu_model_->GetItemCount() - 1,
907                                  button->GetIcon());
908
909    // TODO(estade): set the menu item's tooltip.
910  }
911
912  overflow_menu_.reset(new MenuGtk(this, overflow_menu_model_.get()));
913  signals_.Connect(overflow_menu_->widget(), "button-press-event",
914                   G_CALLBACK(OnOverflowMenuButtonPressThunk), this);
915
916  overflow_button_->SetPaintOverride(GTK_STATE_ACTIVE);
917  overflow_menu_->PopupAsFromKeyEvent(chevron());
918
919  return FALSE;
920}
921
922gboolean BrowserActionsToolbarGtk::OnOverflowMenuButtonPress(
923    GtkWidget* overflow, GdkEventButton* event) {
924  if (event->button != 3)
925    return FALSE;
926
927  GtkWidget* menu_item = GTK_MENU_SHELL(overflow)->active_menu_item;
928  if (!menu_item)
929    return FALSE;
930
931  int item_index = g_list_index(GTK_MENU_SHELL(overflow)->children, menu_item);
932  if (item_index == -1) {
933    NOTREACHED();
934    return FALSE;
935  }
936
937  item_index += gtk_chrome_shrinkable_hbox_get_visible_child_count(
938      GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()));
939  if (profile_->IsOffTheRecord())
940    item_index = model_->IncognitoIndexToOriginal(item_index);
941
942  const Extension* extension = model_->GetExtensionByIndex(item_index);
943  ExtensionButtonMap::iterator it = extension_button_map_.find(
944      extension->id());
945  if (it == extension_button_map_.end()) {
946    NOTREACHED();
947    return FALSE;
948  }
949
950  MenuGtk* menu = it->second.get()->GetContextMenu();
951  if (!menu)
952    return FALSE;
953
954  menu->PopupAsContext(gfx::Point(event->x_root, event->y_root),
955                       event->time);
956  return TRUE;
957}
958
959void BrowserActionsToolbarGtk::OnButtonShowOrHide(GtkWidget* sender) {
960  if (!resize_animation_.is_animating())
961    UpdateChevronVisibility();
962}
963