diff --git a/BUILD.gn b/BUILD.gn index 3f1b3b2febfa1..4e824c5eaa822 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -632,8 +632,6 @@ source_set("electron_lib") { sources += [ "shell/browser/certificate_manager_model.cc", "shell/browser/certificate_manager_model.h", - "shell/browser/ui/gtk/menu_util.cc", - "shell/browser/ui/gtk/menu_util.h", "shell/browser/ui/gtk_util.cc", "shell/browser/ui/gtk_util.h", ] diff --git a/docs/api/tray.md b/docs/api/tray.md index f64d02debf93a..2514931dd0d10 100644 --- a/docs/api/tray.md +++ b/docs/api/tray.md @@ -29,8 +29,9 @@ __Platform Considerations__ __Linux__ -* Tray icon requires support of [StatusNotifierItem](https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/) - in user's desktop environment. +* Tray icon uses [StatusNotifierItem](https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/) + by default, when it is not available in user's desktop environment the + `GtkStatusIcon` will be used instead. * The `click` event is emitted when the tray icon receives activation from user, however the StatusNotifierItem spec does not specify which action would cause an activation, for some environments it is left mouse click, but for diff --git a/filenames.gni b/filenames.gni index 8518fc08214de..19dd6b9d54875 100644 --- a/filenames.gni +++ b/filenames.gni @@ -35,9 +35,15 @@ filenames = { "shell/browser/relauncher_linux.cc", "shell/browser/ui/electron_desktop_window_tree_host_linux.cc", "shell/browser/ui/file_dialog_gtk.cc", + "shell/browser/ui/gtk/menu_gtk.cc", + "shell/browser/ui/gtk/menu_gtk.h", + "shell/browser/ui/gtk/menu_util.cc", + "shell/browser/ui/gtk/menu_util.h", "shell/browser/ui/message_box_gtk.cc", - "shell/browser/ui/tray_icon_gtk.cc", - "shell/browser/ui/tray_icon_gtk.h", + "shell/browser/ui/status_icon_gtk.cc", + "shell/browser/ui/status_icon_gtk.h", + "shell/browser/ui/tray_icon_linux.cc", + "shell/browser/ui/tray_icon_linux.h", "shell/browser/ui/views/client_frame_view_linux.cc", "shell/browser/ui/views/client_frame_view_linux.h", "shell/common/application_info_linux.cc", diff --git a/shell/browser/ui/gtk/menu_gtk.cc b/shell/browser/ui/gtk/menu_gtk.cc new file mode 100644 index 0000000000000..8e73a50569a56 --- /dev/null +++ b/shell/browser/ui/gtk/menu_gtk.cc @@ -0,0 +1,70 @@ +// Copyright 2014 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 "shell/browser/ui/gtk/menu_gtk.h" + +#include + +#include "shell/browser/ui/gtk/menu_util.h" +#include "ui/base/models/menu_model.h" + +namespace electron { + +namespace gtkui { + +MenuGtk::MenuGtk(ui::MenuModel* model) + : menu_model_(model), gtk_menu_(TakeGObject(gtk_menu_new())) { + if (menu_model_) { + BuildSubmenuFromModel(menu_model_, gtk_menu_, + G_CALLBACK(OnMenuItemActivatedThunk), + &block_activation_, this); + Refresh(); + } +} + +MenuGtk::~MenuGtk() { + gtk_widget_destroy(gtk_menu_); +} + +void MenuGtk::Refresh() { + gtk_container_foreach(GTK_CONTAINER(gtk_menu_.get()), SetMenuItemInfo, + &block_activation_); +} + +GtkMenu* MenuGtk::GetGtkMenu() { + return GTK_MENU(gtk_menu_.get()); +} + +void MenuGtk::OnMenuItemActivated(GtkWidget* menu_item) { + if (block_activation_) + return; + + ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item)); + if (!model) { + // There won't be a model for "native" submenus like the "Input Methods" + // context menu. We don't need to handle activation messages for submenus + // anyway, so we can just return here. + DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item))); + return; + } + + // The activate signal is sent to radio items as they get deselected; + // ignore it in this case. + if (GTK_IS_RADIO_MENU_ITEM(menu_item) && + !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) { + return; + } + + int id; + if (!GetMenuItemID(menu_item, &id)) + return; + + // The menu item can still be activated by hotkeys even if it is disabled. + if (model->IsEnabledAt(id)) + ExecuteCommand(model, id); +} + +} // namespace gtkui + +} // namespace electron diff --git a/shell/browser/ui/gtk/menu_gtk.h b/shell/browser/ui/gtk/menu_gtk.h new file mode 100644 index 0000000000000..86ad8083d9b32 --- /dev/null +++ b/shell/browser/ui/gtk/menu_gtk.h @@ -0,0 +1,48 @@ +// Copyright 2014 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. + +#ifndef ELECTRON_SHELL_BROWSER_UI_GTK_MENU_GTK_H_ +#define ELECTRON_SHELL_BROWSER_UI_GTK_MENU_GTK_H_ + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "ui/base/glib/glib_signal.h" +#include "ui/base/glib/scoped_gobject.h" + +typedef struct _GtkMenu GtkMenu; +typedef struct _GtkWidget GtkWidget; + +namespace ui { +class MenuModel; +} + +namespace electron { + +namespace gtkui { + +class MenuGtk { + public: + explicit MenuGtk(ui::MenuModel* model); + virtual ~MenuGtk(); + + // Refreshes all the menu item labels and menu item checked/enabled states. + void Refresh(); + + GtkMenu* GetGtkMenu(); + + private: + // Callback for when a menu item is activated. + CHROMEG_CALLBACK_0(MenuGtk, void, OnMenuItemActivated, GtkWidget*); + + raw_ptr menu_model_; // not owned + ScopedGObject gtk_menu_; + + bool block_activation_ = false; +}; + +} // namespace gtkui + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_UI_GTK_MENU_GTK_H_ diff --git a/shell/browser/ui/status_icon_gtk.cc b/shell/browser/ui/status_icon_gtk.cc new file mode 100644 index 0000000000000..1c8d0024c92e5 --- /dev/null +++ b/shell/browser/ui/status_icon_gtk.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Microsoft, Inc. +// Copyright (c) 2011 The Chromium Authors. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/ui/status_icon_gtk.h" + +#include + +#include "base/strings/utf_string_conversions.h" +#include "shell/browser/ui/gtk/menu_gtk.h" +#include "shell/browser/ui/gtk_util.h" +#include "ui/gfx/image/image_skia.h" + +namespace electron { + +StatusIconGtk::StatusIconGtk() : icon_(TakeGObject(gtk_status_icon_new())) { + g_signal_connect(icon_, "activate", G_CALLBACK(OnClickThunk), this); + g_signal_connect(icon_, "popup_menu", G_CALLBACK(OnContextMenuRequestedThunk), + this); +} + +StatusIconGtk::~StatusIconGtk() = default; + +void StatusIconGtk::SetIcon(const gfx::ImageSkia& image) { + if (image.isNull()) + return; + + GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap(*image.bitmap()); + gtk_status_icon_set_from_pixbuf(icon_, pixbuf); + g_object_unref(pixbuf); +} + +void StatusIconGtk::SetToolTip(const std::u16string& tool_tip) { + gtk_status_icon_set_tooltip_text(icon_, base::UTF16ToUTF8(tool_tip).c_str()); +} + +void StatusIconGtk::UpdatePlatformContextMenu(ui::MenuModel* model) { + if (model) + menu_ = std::make_unique(model); +} + +void StatusIconGtk::RefreshPlatformContextMenu() { + if (menu_) + menu_->Refresh(); +} + +void StatusIconGtk::OnSetDelegate() { + SetIcon(delegate_->GetImage()); + SetToolTip(delegate_->GetToolTip()); + UpdatePlatformContextMenu(delegate_->GetMenuModel()); + gtk_status_icon_set_visible(icon_, TRUE); +} + +void StatusIconGtk::OnClick(GtkStatusIcon* status_icon) { + delegate_->OnClick(); +} + +void StatusIconGtk::OnContextMenuRequested(GtkStatusIcon* status_icon, + guint button, + guint32 activate_time) { + if (menu_.get()) { + gtk_menu_popup(menu_->GetGtkMenu(), nullptr, nullptr, + gtk_status_icon_position_menu, icon_, button, activate_time); + } +} + +} // namespace electron diff --git a/shell/browser/ui/status_icon_gtk.h b/shell/browser/ui/status_icon_gtk.h new file mode 100644 index 0000000000000..beeff7593043c --- /dev/null +++ b/shell/browser/ui/status_icon_gtk.h @@ -0,0 +1,52 @@ +// Copyright (c) 2023 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_UI_STATUS_ICON_GTK_H_ +#define ELECTRON_SHELL_BROWSER_UI_STATUS_ICON_GTK_H_ + +#include + +#include "ui/base/glib/glib_integers.h" +#include "ui/base/glib/glib_signal.h" +#include "ui/base/glib/scoped_gobject.h" +#include "ui/linux/status_icon_linux.h" + +typedef struct _GtkStatusIcon GtkStatusIcon; + +namespace electron { + +namespace gtkui { +class MenuGtk; +} + +class StatusIconGtk : public ui::StatusIconLinux { + public: + StatusIconGtk(); + StatusIconGtk(const StatusIconGtk&) = delete; + StatusIconGtk& operator=(const StatusIconGtk&) = delete; + ~StatusIconGtk() override; + + // ui::StatusIconLinux: + void SetIcon(const gfx::ImageSkia& image) override; + void SetToolTip(const std::u16string& tool_tip) override; + void UpdatePlatformContextMenu(ui::MenuModel* model) override; + void RefreshPlatformContextMenu() override; + void OnSetDelegate() override; + + private: + CHROMEG_CALLBACK_0(StatusIconGtk, void, OnClick, GtkStatusIcon*); + CHROMEG_CALLBACK_2(StatusIconGtk, + void, + OnContextMenuRequested, + GtkStatusIcon*, + guint, + guint); + + std::unique_ptr menu_; + ScopedGObject icon_; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_UI_STATUS_ICON_GTK_H_ diff --git a/shell/browser/ui/tray_icon_gtk.cc b/shell/browser/ui/tray_icon_gtk.cc deleted file mode 100644 index 56b6e1bf1e093..0000000000000 --- a/shell/browser/ui/tray_icon_gtk.cc +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2014 GitHub, Inc. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#include "shell/browser/ui/tray_icon_gtk.h" - -#include "base/strings/utf_string_conversions.h" -#include "chrome/browser/ui/views/status_icons/status_icon_linux_dbus.h" -#include "ui/gfx/image/image_skia_rep.h" - -namespace electron { - -namespace { - -gfx::ImageSkia GetBestImageRep(const gfx::ImageSkia& image) { - image.EnsureRepsForSupportedScales(); - float best_scale = 0.0f; - SkBitmap best_rep; - for (const auto& rep : image.image_reps()) { - if (rep.scale() > best_scale) { - best_scale = rep.scale(); - best_rep = rep.GetBitmap(); - } - } - // All status icon implementations want the image in pixel coordinates, so use - // a scale factor of 1. - return gfx::ImageSkia::CreateFromBitmap(best_rep, 1.0f); -} - -} // namespace - -TrayIconGtk::TrayIconGtk() - : status_icon_(new StatusIconLinuxDbus), - status_icon_type_(StatusIconType::kDbus) { - status_icon_->SetDelegate(this); -} - -TrayIconGtk::~TrayIconGtk() = default; - -void TrayIconGtk::SetImage(const gfx::Image& image) { - image_ = GetBestImageRep(image.AsImageSkia()); - if (status_icon_) - status_icon_->SetIcon(image_); -} - -void TrayIconGtk::SetToolTip(const std::string& tool_tip) { - tool_tip_ = base::UTF8ToUTF16(tool_tip); - if (status_icon_) - status_icon_->SetToolTip(tool_tip_); -} - -void TrayIconGtk::SetContextMenu(ElectronMenuModel* menu_model) { - menu_model_ = menu_model; - if (status_icon_) - status_icon_->UpdatePlatformContextMenu(menu_model_); -} - -const gfx::ImageSkia& TrayIconGtk::GetImage() const { - return image_; -} - -const std::u16string& TrayIconGtk::GetToolTip() const { - return tool_tip_; -} - -ui::MenuModel* TrayIconGtk::GetMenuModel() const { - return menu_model_; -} - -void TrayIconGtk::OnImplInitializationFailed() { - switch (status_icon_type_) { - case StatusIconType::kDbus: - status_icon_ = nullptr; - status_icon_type_ = StatusIconType::kNone; - return; - case StatusIconType::kNone: - NOTREACHED(); - } -} - -void TrayIconGtk::OnClick() { - NotifyClicked(); -} - -bool TrayIconGtk::HasClickAction() { - // Returning true will make the tooltip show as an additional context menu - // item, which makes sense in Chrome but not in most Electron apps. - return false; -} - -// static -TrayIcon* TrayIcon::Create(absl::optional guid) { - return new TrayIconGtk; -} - -} // namespace electron diff --git a/shell/browser/ui/tray_icon_linux.cc b/shell/browser/ui/tray_icon_linux.cc new file mode 100644 index 0000000000000..6f0a9e1dc50ff --- /dev/null +++ b/shell/browser/ui/tray_icon_linux.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2014 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/ui/tray_icon_linux.h" + +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/ui/views/status_icons/status_icon_linux_dbus.h" +#include "shell/browser/ui/status_icon_gtk.h" +#include "ui/gfx/image/image_skia_rep.h" + +namespace electron { + +namespace { + +gfx::ImageSkia GetBestImageRep(const gfx::ImageSkia& image) { + image.EnsureRepsForSupportedScales(); + float best_scale = 0.0f; + SkBitmap best_rep; + for (const auto& rep : image.image_reps()) { + if (rep.scale() > best_scale) { + best_scale = rep.scale(); + best_rep = rep.GetBitmap(); + } + } + // All status icon implementations want the image in pixel coordinates, so use + // a scale factor of 1. + return gfx::ImageSkia::CreateFromBitmap(best_rep, 1.0f); +} + +} // namespace + +TrayIconLinux::TrayIconLinux() + : status_icon_dbus_(new StatusIconLinuxDbus), + status_icon_type_(StatusIconType::kDbus) { + status_icon_dbus_->SetDelegate(this); +} + +TrayIconLinux::~TrayIconLinux() = default; + +void TrayIconLinux::SetImage(const gfx::Image& image) { + image_ = GetBestImageRep(image.AsImageSkia()); + if (auto* status_icon = GetStatusIcon()) + status_icon->SetIcon(image_); +} + +void TrayIconLinux::SetToolTip(const std::string& tool_tip) { + tool_tip_ = base::UTF8ToUTF16(tool_tip); + if (auto* status_icon = GetStatusIcon()) + status_icon->SetToolTip(tool_tip_); +} + +void TrayIconLinux::SetContextMenu(ElectronMenuModel* menu_model) { + menu_model_ = menu_model; + if (auto* status_icon = GetStatusIcon()) + status_icon->UpdatePlatformContextMenu(menu_model_); +} + +const gfx::ImageSkia& TrayIconLinux::GetImage() const { + return image_; +} + +const std::u16string& TrayIconLinux::GetToolTip() const { + return tool_tip_; +} + +ui::MenuModel* TrayIconLinux::GetMenuModel() const { + return menu_model_; +} + +void TrayIconLinux::OnImplInitializationFailed() { + switch (status_icon_type_) { + case StatusIconType::kDbus: + status_icon_dbus_.reset(); + status_icon_gtk_ = std::make_unique(); + status_icon_type_ = StatusIconType::kGtk; + status_icon_gtk_->SetDelegate(this); + return; + case StatusIconType::kGtk: + status_icon_gtk_.reset(); + status_icon_type_ = StatusIconType::kNone; + menu_model_ = nullptr; + return; + case StatusIconType::kNone: + NOTREACHED(); + } +} + +void TrayIconLinux::OnClick() { + NotifyClicked(); +} + +bool TrayIconLinux::HasClickAction() { + // Returning true will make the tooltip show as an additional context menu + // item, which makes sense in Chrome but not in most Electron apps. + return false; +} + +ui::StatusIconLinux* TrayIconLinux::GetStatusIcon() { + switch (status_icon_type_) { + case StatusIconType::kDbus: + return status_icon_dbus_.get(); + case StatusIconType::kGtk: + return status_icon_gtk_.get(); + case StatusIconType::kNone: + return nullptr; + } +} + +// static +TrayIcon* TrayIcon::Create(absl::optional guid) { + return new TrayIconLinux; +} + +} // namespace electron diff --git a/shell/browser/ui/tray_icon_gtk.h b/shell/browser/ui/tray_icon_linux.h similarity index 68% rename from shell/browser/ui/tray_icon_gtk.h rename to shell/browser/ui/tray_icon_linux.h index ccbcd99b29612..76b94433aa946 100644 --- a/shell/browser/ui/tray_icon_gtk.h +++ b/shell/browser/ui/tray_icon_linux.h @@ -2,8 +2,8 @@ // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. -#ifndef ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_GTK_H_ -#define ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_GTK_H_ +#ifndef ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_LINUX_H_ +#define ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_LINUX_H_ #include #include @@ -15,10 +15,12 @@ class StatusIconLinuxDbus; namespace electron { -class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate { +class StatusIconGtk; + +class TrayIconLinux : public TrayIcon, public ui::StatusIconLinux::Delegate { public: - TrayIconGtk(); - ~TrayIconGtk() override; + TrayIconLinux(); + ~TrayIconLinux() override; // TrayIcon: void SetImage(const gfx::Image& image) override; @@ -28,8 +30,6 @@ class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate { // ui::StatusIconLinux::Delegate void OnClick() override; bool HasClickAction() override; - // The following four methods are only used by StatusIconLinuxDbus, which we - // aren't yet using, so they are given stub implementations. const gfx::ImageSkia& GetImage() const override; const std::u16string& GetToolTip() const override; ui::MenuModel* GetMenuModel() const override; @@ -38,10 +38,14 @@ class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate { private: enum class StatusIconType { kDbus, + kGtk, kNone, }; - scoped_refptr status_icon_; + ui::StatusIconLinux* GetStatusIcon(); + + scoped_refptr status_icon_dbus_; + std::unique_ptr status_icon_gtk_; StatusIconType status_icon_type_; gfx::ImageSkia image_; @@ -51,4 +55,4 @@ class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate { } // namespace electron -#endif // ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_GTK_H_ +#endif // ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_LINUX_H_