• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
40 namespace {
41 
42 // The width of the browser action buttons.
43 const int kButtonWidth = 27;
44 
45 // The padding between browser action buttons.
46 const 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).
50 const int kButtonChevronPadding = 2;
51 
52 // The padding to the left, top and bottom of the browser actions toolbar
53 // separator.
54 const int kSeparatorPadding = 2;
55 
56 // Width of the invisible gripper for resizing the toolbar.
57 const int kResizeGripperWidth = 4;
58 
59 const char* kDragTarget = "application/x-chrome-browseraction";
60 
GetDragTargetEntry()61 GtkTargetEntry 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.
WidthForIconCount(gint icon_count)72 gint WidthForIconCount(gint icon_count) {
73   return std::max((kButtonWidth + kButtonPadding) * icon_count - kButtonPadding,
74                   0);
75 }
76 
77 }  // namespace
78 
79 using ui::SimpleMenuModel;
80 
81 class BrowserActionButton : public NotificationObserver,
82                             public ImageLoadingTracker::Observer,
83                             public ExtensionContextMenuModel::PopupDelegate,
84                             public MenuGtk::Delegate {
85  public:
BrowserActionButton(BrowserActionsToolbarGtk * toolbar,const Extension * extension,GtkThemeService * theme_provider)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 
~BrowserActionButton()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 
button()143   GtkWidget* button() { return button_->widget(); }
144 
widget()145   GtkWidget* widget() { return alignment_.get(); }
146 
extension()147   const Extension* extension() { return extension_; }
148 
149   // NotificationObserver implementation.
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)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.
OnImageLoaded(SkBitmap * image,const ExtensionResource & resource,int index)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.
UpdateState()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 
GetIcon()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 
GetContextMenu()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.
StoppedShowing()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 
CommandWillBeExecuted()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.
ShowPopup(bool devtools)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.
InspectPopup(ExtensionAction * action)256   virtual void InspectPopup(ExtensionAction* action) {
257     ShowPopup(true);
258   }
259 
SetImage(GdkPixbuf * image)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 
OnButtonPress(GtkWidget * widget,GdkEventButton * event,BrowserActionButton * action)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 
OnClicked(GtkWidget * widget,BrowserActionButton * action)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 
OnExposeEvent(GtkWidget * widget,GdkEventExpose * event,BrowserActionButton * button)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 
OnDragBegin(GtkWidget * widget,GdkDragContext * drag_context,BrowserActionButton * button)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 
BrowserActionsToolbarGtk(Browser * browser)364 BrowserActionsToolbarGtk::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 
~BrowserActionsToolbarGtk()454 BrowserActionsToolbarGtk::~BrowserActionsToolbarGtk() {
455   if (model_)
456     model_->RemoveObserver(this);
457   button_hbox_.Destroy();
458   hbox_.Destroy();
459 }
460 
GetCurrentTabId()461 int 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 
Update()469 void 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 
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)476 void 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 
SetupDrags()486 void 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 
CreateAllButtons()495 void 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 
SetContainerWidth()505 void BrowserActionsToolbarGtk::SetContainerWidth() {
506   int showing_actions = model_->GetVisibleIconCount();
507   if (showing_actions >= 0)
508     SetButtonHBoxWidth(WidthForIconCount(showing_actions));
509 }
510 
CreateButtonForExtension(const Extension * extension,int index)511 void 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 
GetBrowserActionWidget(const Extension * extension)548 GtkWidget* 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 
RemoveButtonForExtension(const Extension * extension)558 void BrowserActionsToolbarGtk::RemoveButtonForExtension(
559     const Extension* extension) {
560   if (extension_button_map_.erase(extension->id()))
561     UpdateVisibility();
562   UpdateChevronVisibility();
563 }
564 
UpdateVisibility()565 void BrowserActionsToolbarGtk::UpdateVisibility() {
566   if (button_count() == 0)
567     gtk_widget_hide(widget());
568   else
569     gtk_widget_show(widget());
570 }
571 
ShouldDisplayBrowserAction(const Extension * extension)572 bool 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 
HidePopup()579 void BrowserActionsToolbarGtk::HidePopup() {
580   ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup();
581   if (popup)
582     popup->DestroyPopup();
583 }
584 
AnimateToShowNIcons(int count)585 void 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 
BrowserActionAdded(const Extension * extension,int index)592 void 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 
BrowserActionRemoved(const Extension * extension)609 void 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 
BrowserActionMoved(const Extension * extension,int index)626 void 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 
ModelLoaded()645 void BrowserActionsToolbarGtk::ModelLoaded() {
646   SetContainerWidth();
647 }
648 
AnimationProgressed(const ui::Animation * animation)649 void 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 
AnimationEnded(const ui::Animation * animation)659 void BrowserActionsToolbarGtk::AnimationEnded(const ui::Animation* animation) {
660   gtk_widget_set_size_request(button_hbox_.get(), desired_width_, -1);
661   UpdateChevronVisibility();
662 }
663 
IsCommandIdChecked(int command_id) const664 bool BrowserActionsToolbarGtk::IsCommandIdChecked(int command_id) const {
665   return false;
666 }
667 
IsCommandIdEnabled(int command_id) const668 bool BrowserActionsToolbarGtk::IsCommandIdEnabled(int command_id) const {
669   return true;
670 }
671 
GetAcceleratorForCommandId(int command_id,ui::Accelerator * accelerator)672 bool BrowserActionsToolbarGtk::GetAcceleratorForCommandId(
673     int command_id,
674     ui::Accelerator* accelerator) {
675   return false;
676 }
677 
ExecuteCommand(int command_id)678 void 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 
StoppedShowing()700 void BrowserActionsToolbarGtk::StoppedShowing() {
701   overflow_button_->UnsetPaintOverride();
702 }
703 
AlwaysShowIconForCmd(int command_id) const704 bool BrowserActionsToolbarGtk::AlwaysShowIconForCmd(int command_id) const {
705   return true;
706 }
707 
DragStarted(BrowserActionButton * button,GdkDragContext * drag_context)708 void 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 
SetButtonHBoxWidth(int new_width)719 void 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 
UpdateChevronVisibility()726 void 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 
OnDragMotion(GtkWidget * widget,GdkDragContext * drag_context,gint x,gint y,guint time)759 gboolean 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 
OnDragEnd(GtkWidget * button,GdkDragContext * drag_context)780 void 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 
OnDragFailed(GtkWidget * widget,GdkDragContext * drag_context,GtkDragResult result)793 gboolean 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 
OnHierarchyChanged(GtkWidget * widget,GtkWidget * previous_toplevel)803 void 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 
OnSetFocus(GtkWidget * widget,GtkWidget * focus_widget)812 void 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 
OnGripperMotionNotify(GtkWidget * widget,GdkEventMotion * event)824 gboolean 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 
OnGripperExpose(GtkWidget * gripper,GdkEventExpose * expose)840 gboolean 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.
OnGripperEnterNotify(GtkWidget * gripper,GdkEventCrossing * event)850 gboolean 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 
OnGripperLeaveNotify(GtkWidget * gripper,GdkEventCrossing * event)857 gboolean 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 
OnGripperButtonRelease(GtkWidget * gripper,GdkEventButton * event)864 gboolean 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 
OnGripperButtonPress(GtkWidget * gripper,GdkEventButton * event)883 gboolean BrowserActionsToolbarGtk::OnGripperButtonPress(
884     GtkWidget* gripper, GdkEventButton* event) {
885   resize_animation_.Reset();
886 
887   return FALSE;
888 }
889 
OnOverflowButtonPress(GtkWidget * overflow,GdkEventButton * event)890 gboolean 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 
OnOverflowMenuButtonPress(GtkWidget * overflow,GdkEventButton * event)922 gboolean 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 
OnButtonShowOrHide(GtkWidget * sender)959 void BrowserActionsToolbarGtk::OnButtonShowOrHide(GtkWidget* sender) {
960   if (!resize_animation_.is_animating())
961     UpdateChevronVisibility();
962 }
963