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