// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/gtk/browser_actions_toolbar_gtk.h" #include #include #include #include #include "base/bind.h" #include "base/i18n/rtl.h" #include "base/message_loop/message_loop.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/api/commands/command_service.h" #include "chrome/browser/extensions/extension_action.h" #include "chrome/browser/extensions/extension_action_icon_factory.h" #include "chrome/browser/extensions/extension_action_manager.h" #include "chrome/browser/extensions/extension_context_menu_model.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/extensions/extension_util.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sessions/session_tab_helper.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/gtk/browser_window_gtk.h" #include "chrome/browser/ui/gtk/custom_button.h" #include "chrome/browser/ui/gtk/extensions/extension_popup_gtk.h" #include "chrome/browser/ui/gtk/gtk_chrome_button.h" #include "chrome/browser/ui/gtk/gtk_chrome_shrinkable_hbox.h" #include "chrome/browser/ui/gtk/gtk_theme_service.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "chrome/browser/ui/gtk/hover_controller_gtk.h" #include "chrome/browser/ui/gtk/menu_gtk.h" #include "chrome/browser/ui/gtk/view_id_util.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/extensions/extension.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_source.h" #include "extensions/common/manifest_constants.h" #include "grit/theme_resources.h" #include "grit/ui_resources.h" #include "ui/base/accelerators/platform_accelerator_gtk.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas_skia_paint.h" #include "ui/gfx/gtk_compat.h" #include "ui/gfx/gtk_util.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia_operations.h" using extensions::Extension; using extensions::ExtensionActionManager; namespace { // The width of the browser action buttons. const int kButtonWidth = 27; // The padding between browser action buttons. const int kButtonPadding = 4; // The padding to the right of the browser action buttons (between the buttons // and chevron if they are both showing). const int kButtonChevronPadding = 2; // Width of the invisible gripper for resizing the toolbar. const int kResizeGripperWidth = 4; const char kDragTarget[] = "application/x-chrome-browseraction"; GtkTargetEntry GetDragTargetEntry() { GtkTargetEntry drag_target; drag_target.target = const_cast(kDragTarget); drag_target.flags = GTK_TARGET_SAME_APP; drag_target.info = 0; return drag_target; } // The minimum width in pixels of the button hbox if |icon_count| icons are // showing. gint WidthForIconCount(gint icon_count) { return std::max((kButtonWidth + kButtonPadding) * icon_count - kButtonPadding, 0); } } // namespace using ui::SimpleMenuModel; class BrowserActionButton : public content::NotificationObserver, public ExtensionActionIconFactory::Observer, public ExtensionContextMenuModel::PopupDelegate, public MenuGtk::Delegate { public: BrowserActionButton(BrowserActionsToolbarGtk* toolbar, const Extension* extension, GtkThemeService* theme_provider) : toolbar_(toolbar), extension_(extension), image_(NULL), icon_factory_(toolbar->browser()->profile(), extension, browser_action(), this), accel_group_(NULL) { button_.reset(new CustomDrawButton( theme_provider, IDR_BROWSER_ACTION, IDR_BROWSER_ACTION_P, IDR_BROWSER_ACTION_H, 0, NULL)); gtk_widget_set_size_request(button(), kButtonWidth, kButtonWidth); alignment_.Own(gtk_alignment_new(0, 0, 1, 1)); gtk_container_add(GTK_CONTAINER(alignment_.get()), button()); gtk_widget_show(button()); DCHECK(browser_action()); UpdateState(); signals_.Connect(button(), "button-press-event", G_CALLBACK(OnButtonPress), this); signals_.Connect(button(), "clicked", G_CALLBACK(OnClicked), this); signals_.Connect(button(), "drag-begin", G_CALLBACK(OnDragBegin), this); signals_.ConnectAfter(widget(), "expose-event", G_CALLBACK(OnExposeEvent), this); if (toolbar_->browser()->window()) { // If the window exists already, then the browser action button has been // recreated after the window was created, for example when the extension // is reloaded. ConnectBrowserActionPopupAccelerator(); } else { // Window doesn't exist yet, wait for it. signals_.Connect(toolbar->widget(), "realize", G_CALLBACK(OnRealize), this); } registrar_.Add( this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED, content::Source(browser_action())); registrar_.Add( this, chrome::NOTIFICATION_EXTENSION_UNLOADED, content::Source( toolbar->browser()->profile()->GetOriginalProfile())); registrar_.Add( this, chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED, content::Source( toolbar->browser()->profile()->GetOriginalProfile())); registrar_.Add( this, chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED, content::Source( toolbar->browser()->profile()->GetOriginalProfile())); } virtual ~BrowserActionButton() { DisconnectBrowserActionPopupAccelerator(); alignment_.Destroy(); } GtkWidget* button() { return button_->widget(); } GtkWidget* widget() { return alignment_.get(); } const Extension* extension() { return extension_; } // NotificationObserver implementation. virtual void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) OVERRIDE { switch (type) { case chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED: UpdateState(); break; case chrome::NOTIFICATION_EXTENSION_UNLOADED: case chrome::NOTIFICATION_WINDOW_CLOSED: DisconnectBrowserActionPopupAccelerator(); break; case chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED: case chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED: { std::pair* payload = content::Details >( details).ptr(); if (extension_->id() == payload->first && payload->second == extensions::manifest_values::kBrowserActionCommandEvent) { if (type == chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED) ConnectBrowserActionPopupAccelerator(); else DisconnectBrowserActionPopupAccelerator(); } break; } default: NOTREACHED(); break; } } // ExtensionActionIconFactory::Observer implementation. virtual void OnIconUpdated() OVERRIDE { UpdateState(); } // Updates the button based on the latest state from the associated // browser action. void UpdateState() { int tab_id = toolbar_->GetCurrentTabId(); if (tab_id < 0) return; std::string tooltip = browser_action()->GetTitle(tab_id); if (tooltip.empty()) gtk_widget_set_has_tooltip(button(), FALSE); else gtk_widget_set_tooltip_text(button(), tooltip.c_str()); enabled_ = browser_action()->GetIsVisible(tab_id); if (!enabled_) button_->SetPaintOverride(GTK_STATE_INSENSITIVE); else button_->UnsetPaintOverride(); gfx::Image image = icon_factory_.GetIcon(tab_id); if (!image.IsEmpty()) { if (enabled_) { SetImage(image); } else { SetImage(gfx::Image(gfx::ImageSkiaOperations::CreateTransparentImage( image.AsImageSkia(), .25))); } } gtk_widget_queue_draw(button()); } gfx::Image GetIcon() { return icon_factory_.GetIcon(toolbar_->GetCurrentTabId()); } MenuGtk* GetContextMenu() { if (!extension_->ShowConfigureContextMenus()) return NULL; context_menu_model_ = new ExtensionContextMenuModel(extension_, toolbar_->browser(), this); context_menu_.reset( new MenuGtk(this, context_menu_model_.get())); return context_menu_.get(); } private: // Activate the browser action. Returns true if a popup was shown. Showing the // popup will grant tab permissions if |should_grant| is true. Popup's shown // via an API should not grant permissions. bool Activate(GtkWidget* widget, bool should_grant) { ExtensionToolbarModel* model = toolbar_->model(); const Extension* extension = extension_; Browser* browser = toolbar_->browser(); GURL popup_url; switch (model->ExecuteBrowserAction( extension, browser, &popup_url, should_grant)) { case ExtensionToolbarModel::ACTION_NONE: break; case ExtensionToolbarModel::ACTION_SHOW_POPUP: ExtensionPopupGtk::Show(popup_url, browser, widget, ExtensionPopupGtk::SHOW); return true; } return false; } // MenuGtk::Delegate implementation. virtual void StoppedShowing() OVERRIDE { if (enabled_) button_->UnsetPaintOverride(); else button_->SetPaintOverride(GTK_STATE_INSENSITIVE); // If the context menu was showing for the overflow menu, re-assert the // grab that was shadowed. if (toolbar_->overflow_menu_.get()) gtk_util::GrabAllInput(toolbar_->overflow_menu_->widget()); } virtual void CommandWillBeExecuted() OVERRIDE { // If the context menu was showing for the overflow menu, and a command // is executed, then stop showing the overflow menu. if (toolbar_->overflow_menu_.get()) toolbar_->overflow_menu_->Cancel(); } // ExtensionContextMenuModel::PopupDelegate implementation. virtual void InspectPopup(ExtensionAction* action) OVERRIDE { GURL popup_url = action->GetPopupUrl(toolbar_->GetCurrentTabId()); ExtensionPopupGtk::Show(popup_url, toolbar_->browser(), widget(), ExtensionPopupGtk::SHOW_AND_INSPECT); } void SetImage(const gfx::Image& image) { if (!image_) { image_ = gtk_image_new_from_pixbuf(image.ToGdkPixbuf()); gtk_button_set_image(GTK_BUTTON(button()), image_); } else { gtk_image_set_from_pixbuf(GTK_IMAGE(image_), image.ToGdkPixbuf()); } } static gboolean OnButtonPress(GtkWidget* widget, GdkEventButton* event, BrowserActionButton* button) { if (event->button != 3) return FALSE; MenuGtk* menu = button->GetContextMenu(); if (!menu) return FALSE; button->button_->SetPaintOverride(GTK_STATE_ACTIVE); menu->PopupForWidget(widget, event->button, event->time); return TRUE; } static void OnClicked(GtkWidget* widget, BrowserActionButton* button) { if (button->enabled_) button->Activate(widget, true); } static gboolean OnExposeEvent(GtkWidget* widget, GdkEventExpose* event, BrowserActionButton* button) { int tab_id = button->toolbar_->GetCurrentTabId(); if (tab_id < 0) return FALSE; ExtensionAction* action = button->browser_action(); if (action->GetBadgeText(tab_id).empty()) return FALSE; gfx::CanvasSkiaPaint canvas(event, false); GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); action->PaintBadge(&canvas, gfx::Rect(allocation), tab_id); return FALSE; } static void OnDragBegin(GtkWidget* widget, GdkDragContext* drag_context, BrowserActionButton* button) { // Simply pass along the notification to the toolbar. The point of this // function is to tell the toolbar which BrowserActionButton initiated the // drag. button->toolbar_->DragStarted(button, drag_context); } // The accelerator handler for when the shortcuts to open the popup is struck. static gboolean OnGtkAccelerator(GtkAccelGroup* accel_group, GObject* acceleratable, guint keyval, GdkModifierType modifier, BrowserActionButton* button) { // Open the popup for this extension. GtkWidget* anchor = button->widget(); // The anchor might be in the overflow menu. Then we point to the chevron. if (!gtk_widget_get_visible(anchor)) anchor = button->toolbar_->chevron(); button->Activate(anchor, true); return TRUE; } // The handler for when the browser action is realized. |user_data| contains a // pointer to the BrowserAction shown. static void OnRealize(GtkWidget* widget, void* user_data) { BrowserActionButton* button = static_cast(user_data); button->ConnectBrowserActionPopupAccelerator(); } // Connect the accelerator for the browser action popup. void ConnectBrowserActionPopupAccelerator() { extensions::CommandService* command_service = extensions::CommandService::Get(toolbar_->browser()->profile()); extensions::Command command; if (command_service->GetBrowserActionCommand(extension_->id(), extensions::CommandService::ACTIVE_ONLY, &command, NULL)) { // Found the browser action shortcut command, register it. keybinding_ = command.accelerator(); gfx::NativeWindow window = toolbar_->browser()->window()->GetNativeWindow(); accel_group_ = gtk_accel_group_new(); gtk_window_add_accel_group(window, accel_group_); gtk_accel_group_connect( accel_group_, ui::GetGdkKeyCodeForAccelerator(keybinding_), ui::GetGdkModifierForAccelerator(keybinding_), GtkAccelFlags(0), g_cclosure_new(G_CALLBACK(OnGtkAccelerator), this, NULL)); // Since we've added an accelerator, we'll need to unregister it before // the window is closed, so we listen for the window being closed. registrar_.Add(this, chrome::NOTIFICATION_WINDOW_CLOSED, content::Source(window)); } } // Disconnect the accelerator for the browser action popup and delete clean up // the accelerator group registration. void DisconnectBrowserActionPopupAccelerator() { if (accel_group_) { gfx::NativeWindow window = toolbar_->browser()->window()->GetNativeWindow(); gtk_accel_group_disconnect_key( accel_group_, ui::GetGdkKeyCodeForAccelerator(keybinding_), GetGdkModifierForAccelerator(keybinding_)); gtk_window_remove_accel_group(window, accel_group_); g_object_unref(accel_group_); accel_group_ = NULL; keybinding_ = ui::Accelerator(); // We've removed the accelerator, so no need to listen to this anymore. registrar_.Remove(this, chrome::NOTIFICATION_WINDOW_CLOSED, content::Source(window)); } } ExtensionAction* browser_action() const { return ExtensionActionManager::Get(toolbar_->browser()->profile())-> GetBrowserAction(*extension_); } // The toolbar containing this button. BrowserActionsToolbarGtk* toolbar_; // The extension that contains this browser action. const Extension* extension_; // The button for this browser action. scoped_ptr button_; // Whether the browser action is enabled (equivalent to whether a page action // is visible). bool enabled_; // The top level widget (parent of |button_|). ui::OwnedWidgetGtk alignment_; // The one image subwidget in |button_|. We keep this out so we don't alter // the widget hierarchy while changing the button image because changing the // GTK widget hierarchy invalidates all tooltips and several popular // extensions change browser action icon in a loop. GtkWidget* image_; // The object that will be used to get the browser action icon for us. // It may load the icon asynchronously (in which case the initial icon // returned by the factory will be transparent), so we have to observe it for // updates to the icon. ExtensionActionIconFactory icon_factory_; // Same as |default_icon_|, but stored as SkBitmap. SkBitmap default_skbitmap_; ui::GtkSignalRegistrar signals_; content::NotificationRegistrar registrar_; // The accelerator group used to handle accelerators, owned by this object. GtkAccelGroup* accel_group_; // The keybinding accelerator registered to show the browser action popup. ui::Accelerator keybinding_; // The context menu view and model for this extension action. scoped_ptr context_menu_; scoped_refptr context_menu_model_; friend class BrowserActionsToolbarGtk; }; // BrowserActionsToolbarGtk ---------------------------------------------------- BrowserActionsToolbarGtk::BrowserActionsToolbarGtk(Browser* browser) : browser_(browser), profile_(browser->profile()), theme_service_(GtkThemeService::GetFrom(browser->profile())), model_(NULL), hbox_(gtk_hbox_new(FALSE, 0)), button_hbox_(gtk_chrome_shrinkable_hbox_new(TRUE, FALSE, kButtonPadding)), drag_button_(NULL), drop_index_(-1), resize_animation_(this), desired_width_(0), start_width_(0), weak_factory_(this) { ExtensionService* extension_service = extensions::ExtensionSystem::Get(profile_)->extension_service(); if (!extension_service) return; overflow_button_.reset(new CustomDrawButton( theme_service_, IDR_BROWSER_ACTIONS_OVERFLOW, IDR_BROWSER_ACTIONS_OVERFLOW_P, IDR_BROWSER_ACTIONS_OVERFLOW_H, 0, gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE))); GtkWidget* gripper = gtk_button_new(); gtk_widget_set_size_request(gripper, kResizeGripperWidth, -1); gtk_widget_set_can_focus(gripper, FALSE); gtk_widget_add_events(gripper, GDK_POINTER_MOTION_MASK); signals_.Connect(gripper, "motion-notify-event", G_CALLBACK(OnGripperMotionNotifyThunk), this); signals_.Connect(gripper, "expose-event", G_CALLBACK(OnGripperExposeThunk), this); signals_.Connect(gripper, "enter-notify-event", G_CALLBACK(OnGripperEnterNotifyThunk), this); signals_.Connect(gripper, "leave-notify-event", G_CALLBACK(OnGripperLeaveNotifyThunk), this); signals_.Connect(gripper, "button-release-event", G_CALLBACK(OnGripperButtonReleaseThunk), this); signals_.Connect(gripper, "button-press-event", G_CALLBACK(OnGripperButtonPressThunk), this); signals_.Connect(chevron(), "button-press-event", G_CALLBACK(OnOverflowButtonPressThunk), this); // |overflow_alignment| adds padding to the right of the browser action // buttons, but only appears when the overflow menu is showing. overflow_alignment_.Own(gtk_alignment_new(0, 0, 1, 1)); gtk_container_add(GTK_CONTAINER(overflow_alignment_.get()), chevron()); // |overflow_area_| holds the overflow chevron and the separator, which // is only shown in GTK+ theme mode. overflow_area_.Own(gtk_hbox_new(FALSE, 0)); gtk_box_pack_start(GTK_BOX(overflow_area_.get()), overflow_alignment_.get(), FALSE, FALSE, 0); separator_.Own(gtk_vseparator_new()); gtk_box_pack_start(GTK_BOX(overflow_area_.get()), separator_.get(), FALSE, FALSE, 0); gtk_widget_set_no_show_all(separator_.get(), TRUE); gtk_widget_show_all(overflow_area_.get()); gtk_widget_set_no_show_all(overflow_area_.get(), TRUE); gtk_box_pack_start(GTK_BOX(hbox_.get()), gripper, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox_.get()), button_hbox_.get(), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(hbox_.get()), overflow_area_.get(), FALSE, FALSE, 0); model_ = extension_service->toolbar_model(); model_->AddObserver(this); SetupDrags(); if (model_->extensions_initialized()) { CreateAllButtons(); SetContainerWidth(); } // We want to connect to "set-focus" on the toplevel window; we have to wait // until we are added to a toplevel window to do so. signals_.Connect(widget(), "hierarchy-changed", G_CALLBACK(OnHierarchyChangedThunk), this); ViewIDUtil::SetID(button_hbox_.get(), VIEW_ID_BROWSER_ACTION_TOOLBAR); registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, content::Source(theme_service_)); theme_service_->InitThemesFor(this); } BrowserActionsToolbarGtk::~BrowserActionsToolbarGtk() { if (model_) model_->RemoveObserver(this); button_hbox_.Destroy(); hbox_.Destroy(); } int BrowserActionsToolbarGtk::GetCurrentTabId() const { content::WebContents* active_tab = browser_->tab_strip_model()->GetActiveWebContents(); if (!active_tab) return -1; return SessionTabHelper::FromWebContents(active_tab)->session_id().id(); } void BrowserActionsToolbarGtk::Update() { for (ExtensionButtonMap::iterator iter = extension_button_map_.begin(); iter != extension_button_map_.end(); ++iter) { iter->second->UpdateState(); } } void BrowserActionsToolbarGtk::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { DCHECK(chrome::NOTIFICATION_BROWSER_THEME_CHANGED == type); gtk_widget_set_visible(separator_.get(), theme_service_->UsingNativeTheme()); } void BrowserActionsToolbarGtk::SetupDrags() { GtkTargetEntry drag_target = GetDragTargetEntry(); gtk_drag_dest_set(button_hbox_.get(), GTK_DEST_DEFAULT_DROP, &drag_target, 1, GDK_ACTION_MOVE); signals_.Connect(button_hbox_.get(), "drag-motion", G_CALLBACK(OnDragMotionThunk), this); } void BrowserActionsToolbarGtk::CreateAllButtons() { extension_button_map_.clear(); int i = 0; const extensions::ExtensionList& toolbar_items = model_->toolbar_items(); for (extensions::ExtensionList::const_iterator iter = toolbar_items.begin(); iter != toolbar_items.end(); ++iter) { CreateButtonForExtension(iter->get(), i++); } } void BrowserActionsToolbarGtk::SetContainerWidth() { int showing_actions = model_->GetVisibleIconCount(); if (showing_actions >= 0) SetButtonHBoxWidth(WidthForIconCount(showing_actions)); } void BrowserActionsToolbarGtk::CreateButtonForExtension( const Extension* extension, int index) { if (!ShouldDisplayBrowserAction(extension)) return; if (profile_->IsOffTheRecord()) index = model_->OriginalIndexToIncognito(index); RemoveButtonForExtension(extension); linked_ptr button( new BrowserActionButton(this, extension, theme_service_)); gtk_chrome_shrinkable_hbox_pack_start( GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()), button->widget(), 0); gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button->widget(), index); extension_button_map_[extension->id()] = button; GtkTargetEntry drag_target = GetDragTargetEntry(); gtk_drag_source_set(button->button(), GDK_BUTTON1_MASK, &drag_target, 1, GDK_ACTION_MOVE); // We ignore whether the drag was a "success" or "failure" in Gtk's opinion. signals_.Connect(button->button(), "drag-end", G_CALLBACK(&OnDragEndThunk), this); signals_.Connect(button->button(), "drag-failed", G_CALLBACK(&OnDragFailedThunk), this); // Any time a browser action button is shown or hidden we have to update // the chevron state. signals_.Connect(button->widget(), "show", G_CALLBACK(&OnButtonShowOrHideThunk), this); signals_.Connect(button->widget(), "hide", G_CALLBACK(&OnButtonShowOrHideThunk), this); gtk_widget_show(button->widget()); UpdateVisibility(); } BrowserActionButton* BrowserActionsToolbarGtk::GetBrowserActionButton( const Extension* extension) { ExtensionButtonMap::iterator it = extension_button_map_.find( extension->id()); return it == extension_button_map_.end() ? NULL : it->second.get(); } GtkWidget* BrowserActionsToolbarGtk::GetBrowserActionWidget( const Extension* extension) { BrowserActionButton* button = GetBrowserActionButton(extension); return button == NULL ? NULL : button->widget(); } void BrowserActionsToolbarGtk::RemoveButtonForExtension( const Extension* extension) { if (extension_button_map_.erase(extension->id())) UpdateVisibility(); UpdateChevronVisibility(); } void BrowserActionsToolbarGtk::UpdateVisibility() { gtk_widget_set_visible(widget(), button_count() != 0); } bool BrowserActionsToolbarGtk::ShouldDisplayBrowserAction( const Extension* extension) { // Only display incognito-enabled extensions while in incognito mode. return (!profile_->IsOffTheRecord() || extension_util:: IsIncognitoEnabled( extension->id(), extensions::ExtensionSystem::Get(profile_)->extension_service())); } void BrowserActionsToolbarGtk::HidePopup() { ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup(); if (popup) popup->DestroyPopup(); } void BrowserActionsToolbarGtk::AnimateToShowNIcons(int count) { desired_width_ = WidthForIconCount(count); GtkAllocation allocation; gtk_widget_get_allocation(button_hbox_.get(), &allocation); start_width_ = allocation.width; resize_animation_.Reset(); resize_animation_.Show(); } void BrowserActionsToolbarGtk::BrowserActionAdded(const Extension* extension, int index) { overflow_menu_.reset(); CreateButtonForExtension(extension, index); // If we are still initializing the container, don't bother animating. if (!model_->extensions_initialized()) return; // Animate the addition if we are showing all browser action buttons. if (!gtk_widget_get_visible(overflow_area_.get())) { AnimateToShowNIcons(button_count()); model_->SetVisibleIconCount(button_count()); } } void BrowserActionsToolbarGtk::BrowserActionRemoved( const Extension* extension) { overflow_menu_.reset(); if (drag_button_ != NULL) { // Break the current drag. gtk_grab_remove(button_hbox_.get()); } RemoveButtonForExtension(extension); if (!gtk_widget_get_visible(overflow_area_.get())) { AnimateToShowNIcons(button_count()); model_->SetVisibleIconCount(button_count()); } } void BrowserActionsToolbarGtk::BrowserActionMoved(const Extension* extension, int index) { // We initiated this move action, and have already moved the button. if (drag_button_ != NULL) return; GtkWidget* button_widget = GetBrowserActionWidget(extension); if (!button_widget) { if (ShouldDisplayBrowserAction(extension)) NOTREACHED(); return; } if (profile_->IsOffTheRecord()) index = model_->OriginalIndexToIncognito(index); gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button_widget, index); } bool BrowserActionsToolbarGtk::BrowserActionShowPopup( const Extension* extension) { // Do not override other popups and only show in active window. if (ExtensionPopupGtk::get_current_extension_popup() || !browser_->window()->IsActive()) { return false; } BrowserActionButton* button = GetBrowserActionButton(extension); if (button == NULL || button->widget() == NULL) return false; GtkWidget* anchor = button->widget(); if (!gtk_widget_get_visible(anchor)) anchor = button->toolbar_->chevron(); return button->Activate(anchor, false); } void BrowserActionsToolbarGtk::ModelLoaded() { SetContainerWidth(); } void BrowserActionsToolbarGtk::AnimationProgressed( const gfx::Animation* animation) { int width = start_width_ + (desired_width_ - start_width_) * animation->GetCurrentValue(); gtk_widget_set_size_request(button_hbox_.get(), width, -1); if (width == desired_width_) resize_animation_.Reset(); } void BrowserActionsToolbarGtk::AnimationEnded(const gfx::Animation* animation) { gtk_widget_set_size_request(button_hbox_.get(), desired_width_, -1); UpdateChevronVisibility(); } bool BrowserActionsToolbarGtk::IsCommandIdChecked(int command_id) const { return false; } bool BrowserActionsToolbarGtk::IsCommandIdEnabled(int command_id) const { const Extension* extension = model_->toolbar_items()[command_id].get(); return ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension) ->GetIsVisible(GetCurrentTabId()); } bool BrowserActionsToolbarGtk::GetAcceleratorForCommandId( int command_id, ui::Accelerator* accelerator) { return false; } void BrowserActionsToolbarGtk::ExecuteCommand(int command_id, int event_flags) { const Extension* extension = model_->toolbar_items()[command_id].get(); GURL popup_url; switch (model_->ExecuteBrowserAction( extension, browser(), &popup_url, true)) { case ExtensionToolbarModel::ACTION_NONE: break; case ExtensionToolbarModel::ACTION_SHOW_POPUP: ExtensionPopupGtk::Show(popup_url, browser(), chevron(), ExtensionPopupGtk::SHOW); break; } } void BrowserActionsToolbarGtk::StoppedShowing() { overflow_button_->UnsetPaintOverride(); } bool BrowserActionsToolbarGtk::AlwaysShowIconForCmd(int command_id) const { return true; } void BrowserActionsToolbarGtk::DragStarted(BrowserActionButton* button, GdkDragContext* drag_context) { // No representation of the widget following the cursor. GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1); gtk_drag_set_icon_pixbuf(drag_context, pixbuf, 0, 0); g_object_unref(pixbuf); DCHECK(!drag_button_); drag_button_ = button; } void BrowserActionsToolbarGtk::SetButtonHBoxWidth(int new_width) { gint max_width = WidthForIconCount(button_count()); new_width = std::min(max_width, new_width); new_width = std::max(new_width, 0); gtk_widget_set_size_request(button_hbox_.get(), new_width, -1); } void BrowserActionsToolbarGtk::UpdateChevronVisibility() { int showing_icon_count = gtk_chrome_shrinkable_hbox_get_visible_child_count( GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); if (showing_icon_count == 0) { gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_.get()), 0, 0, 0, 0); } else { gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_.get()), 0, 0, kButtonChevronPadding, 0); } if (button_count() > showing_icon_count) { if (!gtk_widget_get_visible(overflow_area_.get())) { if (drag_button_) { // During drags, when the overflow chevron shows for the first time, // take that much space away from |button_hbox_| to make the drag look // smoother. GtkRequisition req; gtk_widget_size_request(chevron(), &req); gint overflow_width = req.width; gtk_widget_size_request(button_hbox_.get(), &req); gint button_hbox_width = req.width; button_hbox_width = std::max(button_hbox_width - overflow_width, 0); gtk_widget_set_size_request(button_hbox_.get(), button_hbox_width, -1); } gtk_widget_show(overflow_area_.get()); } } else { gtk_widget_hide(overflow_area_.get()); } } gboolean BrowserActionsToolbarGtk::OnDragMotion(GtkWidget* widget, GdkDragContext* drag_context, gint x, gint y, guint time) { // Only handle drags we initiated. if (!drag_button_) return FALSE; if (base::i18n::IsRTL()) { GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); x = allocation.width - x; } drop_index_ = x < kButtonWidth ? 0 : x / (kButtonWidth + kButtonPadding); // We will go ahead and reorder the child in order to provide visual feedback // to the user. We don't inform the model that it has moved until the drag // ends. gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), drag_button_->widget(), drop_index_); gdk_drag_status(drag_context, GDK_ACTION_MOVE, time); return TRUE; } void BrowserActionsToolbarGtk::OnDragEnd(GtkWidget* button, GdkDragContext* drag_context) { if (drop_index_ != -1) { if (profile_->IsOffTheRecord()) drop_index_ = model_->IncognitoIndexToOriginal(drop_index_); model_->MoveBrowserAction(drag_button_->extension(), drop_index_); } drag_button_ = NULL; drop_index_ = -1; } gboolean BrowserActionsToolbarGtk::OnDragFailed(GtkWidget* widget, GdkDragContext* drag_context, GtkDragResult result) { // We connect to this signal and return TRUE so that the default failure // animation (wherein the drag widget floats back to the start of the drag) // does not show, and the drag-end signal is emitted immediately instead of // several seconds later. return TRUE; } void BrowserActionsToolbarGtk::OnHierarchyChanged( GtkWidget* widget, GtkWidget* previous_toplevel) { GtkWidget* toplevel = gtk_widget_get_toplevel(widget); if (!gtk_widget_is_toplevel(toplevel)) return; signals_.Connect(toplevel, "set-focus", G_CALLBACK(OnSetFocusThunk), this); } void BrowserActionsToolbarGtk::OnSetFocus(GtkWidget* widget, GtkWidget* focus_widget) { ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup(); // The focus of the parent window has changed. Close the popup. Delay the hide // because it will destroy the RenderViewHost, which may still be on the // call stack. if (!popup || popup->being_inspected()) return; base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(&BrowserActionsToolbarGtk::HidePopup, weak_factory_.GetWeakPtr())); } gboolean BrowserActionsToolbarGtk::OnGripperMotionNotify( GtkWidget* widget, GdkEventMotion* event) { if (!(event->state & GDK_BUTTON1_MASK)) return FALSE; // Calculate how much the user dragged the gripper and subtract that off the // button container's width. int distance_dragged; if (base::i18n::IsRTL()) { distance_dragged = -event->x; } else { GtkAllocation widget_allocation; gtk_widget_get_allocation(widget, &widget_allocation); distance_dragged = event->x - widget_allocation.width; } GtkAllocation button_hbox_allocation; gtk_widget_get_allocation(button_hbox_.get(), &button_hbox_allocation); gint new_width = button_hbox_allocation.width - distance_dragged; SetButtonHBoxWidth(new_width); return FALSE; } gboolean BrowserActionsToolbarGtk::OnGripperExpose(GtkWidget* gripper, GdkEventExpose* expose) { return TRUE; } // These three signal handlers (EnterNotify, LeaveNotify, and ButtonRelease) // are used to give the gripper the resize cursor. Since it doesn't have its // own window, we have to set the cursor whenever the pointer moves into the // button or leaves the button, and be sure to leave it on when the user is // dragging. gboolean BrowserActionsToolbarGtk::OnGripperEnterNotify( GtkWidget* gripper, GdkEventCrossing* event) { gdk_window_set_cursor(gtk_widget_get_window(gripper), gfx::GetCursor(GDK_SB_H_DOUBLE_ARROW)); return FALSE; } gboolean BrowserActionsToolbarGtk::OnGripperLeaveNotify( GtkWidget* gripper, GdkEventCrossing* event) { if (!(event->state & GDK_BUTTON1_MASK)) gdk_window_set_cursor(gtk_widget_get_window(gripper), NULL); return FALSE; } gboolean BrowserActionsToolbarGtk::OnGripperButtonRelease( GtkWidget* gripper, GdkEventButton* event) { GtkAllocation allocation; gtk_widget_get_allocation(gripper, &allocation); gfx::Rect gripper_rect(0, 0, allocation.width, allocation.height); gfx::Point release_point(event->x, event->y); if (!gripper_rect.Contains(release_point)) gdk_window_set_cursor(gtk_widget_get_window(gripper), NULL); // After the user resizes the toolbar, we want to smartly resize it to be // the perfect size to fit the buttons. int visible_icon_count = gtk_chrome_shrinkable_hbox_get_visible_child_count( GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); AnimateToShowNIcons(visible_icon_count); model_->SetVisibleIconCount(visible_icon_count); return FALSE; } gboolean BrowserActionsToolbarGtk::OnGripperButtonPress( GtkWidget* gripper, GdkEventButton* event) { resize_animation_.Reset(); return FALSE; } gboolean BrowserActionsToolbarGtk::OnOverflowButtonPress( GtkWidget* overflow, GdkEventButton* event) { overflow_menu_model_.reset(new SimpleMenuModel(this)); int visible_icon_count = gtk_chrome_shrinkable_hbox_get_visible_child_count( GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); for (int i = visible_icon_count; i < button_count(); ++i) { int model_index = i; if (profile_->IsOffTheRecord()) model_index = model_->IncognitoIndexToOriginal(i); const Extension* extension = model_->toolbar_items()[model_index].get(); BrowserActionButton* button = extension_button_map_[extension->id()].get(); overflow_menu_model_->AddItem(model_index, UTF8ToUTF16(extension->name())); overflow_menu_model_->SetIcon(overflow_menu_model_->GetItemCount() - 1, button->GetIcon()); // TODO(estade): set the menu item's tooltip. } overflow_menu_.reset(new MenuGtk(this, overflow_menu_model_.get())); signals_.Connect(overflow_menu_->widget(), "button-press-event", G_CALLBACK(OnOverflowMenuButtonPressThunk), this); overflow_button_->SetPaintOverride(GTK_STATE_ACTIVE); overflow_menu_->PopupAsFromKeyEvent(chevron()); return FALSE; } gboolean BrowserActionsToolbarGtk::OnOverflowMenuButtonPress( GtkWidget* overflow, GdkEventButton* event) { if (event->button != 3) return FALSE; GtkWidget* menu_item = GTK_MENU_SHELL(overflow)->active_menu_item; if (!menu_item) return FALSE; int item_index = g_list_index(GTK_MENU_SHELL(overflow)->children, menu_item); if (item_index == -1) { NOTREACHED(); return FALSE; } item_index += gtk_chrome_shrinkable_hbox_get_visible_child_count( GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); if (profile_->IsOffTheRecord()) item_index = model_->IncognitoIndexToOriginal(item_index); const Extension* extension = model_->toolbar_items()[item_index].get(); BrowserActionButton* button = GetBrowserActionButton(extension); if (button == NULL) { NOTREACHED(); return FALSE; } MenuGtk* menu = button->GetContextMenu(); if (!menu) return FALSE; menu->PopupAsContext(gfx::Point(event->x_root, event->y_root), event->time); return TRUE; } void BrowserActionsToolbarGtk::OnButtonShowOrHide(GtkWidget* sender) { if (!resize_animation_.is_animating()) UpdateChevronVisibility(); }