fix: fallback to GtkStatusIcon when app indicator is not supported (#36815)

* chore: get ready for multi backend tray

* fix: fallback to GtkStatusIcon when app indicator is not supported

* chore: use smart pointers
This commit is contained in:
Cheng Zhao 2023-01-26 19:15:55 +09:00 committed by GitHub
parent 7d46d3ec9d
commit c303135b02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 377 additions and 111 deletions

View file

@ -632,8 +632,6 @@ source_set("electron_lib") {
sources += [ sources += [
"shell/browser/certificate_manager_model.cc", "shell/browser/certificate_manager_model.cc",
"shell/browser/certificate_manager_model.h", "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.cc",
"shell/browser/ui/gtk_util.h", "shell/browser/ui/gtk_util.h",
] ]

View file

@ -29,8 +29,9 @@ __Platform Considerations__
__Linux__ __Linux__
* Tray icon requires support of [StatusNotifierItem](https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/) * Tray icon uses [StatusNotifierItem](https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/)
in user's desktop environment. 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 * The `click` event is emitted when the tray icon receives activation from
user, however the StatusNotifierItem spec does not specify which action would user, however the StatusNotifierItem spec does not specify which action would
cause an activation, for some environments it is left mouse click, but for cause an activation, for some environments it is left mouse click, but for

View file

@ -35,9 +35,15 @@ filenames = {
"shell/browser/relauncher_linux.cc", "shell/browser/relauncher_linux.cc",
"shell/browser/ui/electron_desktop_window_tree_host_linux.cc", "shell/browser/ui/electron_desktop_window_tree_host_linux.cc",
"shell/browser/ui/file_dialog_gtk.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/message_box_gtk.cc",
"shell/browser/ui/tray_icon_gtk.cc", "shell/browser/ui/status_icon_gtk.cc",
"shell/browser/ui/tray_icon_gtk.h", "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.cc",
"shell/browser/ui/views/client_frame_view_linux.h", "shell/browser/ui/views/client_frame_view_linux.h",
"shell/common/application_info_linux.cc", "shell/common/application_info_linux.cc",

View file

@ -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 <gtk/gtk.h>
#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

View file

@ -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<ui::MenuModel> menu_model_; // not owned
ScopedGObject<GtkWidget> gtk_menu_;
bool block_activation_ = false;
};
} // namespace gtkui
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_GTK_MENU_GTK_H_

View file

@ -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 <gtk/gtk.h>
#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<gtkui::MenuGtk>(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

View file

@ -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 <memory>
#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<gtkui::MenuGtk> menu_;
ScopedGObject<GtkStatusIcon> icon_;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_STATUS_ICON_GTK_H_

View file

@ -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<UUID> guid) {
return new TrayIconGtk;
}
} // namespace electron

View file

@ -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<StatusIconGtk>();
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<UUID> guid) {
return new TrayIconLinux;
}
} // namespace electron

View file

@ -2,8 +2,8 @@
// Use of this source code is governed by the MIT license that can be // Use of this source code is governed by the MIT license that can be
// found in the LICENSE file. // found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_GTK_H_ #ifndef ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_LINUX_H_
#define ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_GTK_H_ #define ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_LINUX_H_
#include <memory> #include <memory>
#include <string> #include <string>
@ -15,10 +15,12 @@ class StatusIconLinuxDbus;
namespace electron { namespace electron {
class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate { class StatusIconGtk;
class TrayIconLinux : public TrayIcon, public ui::StatusIconLinux::Delegate {
public: public:
TrayIconGtk(); TrayIconLinux();
~TrayIconGtk() override; ~TrayIconLinux() override;
// TrayIcon: // TrayIcon:
void SetImage(const gfx::Image& image) override; void SetImage(const gfx::Image& image) override;
@ -28,8 +30,6 @@ class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate {
// ui::StatusIconLinux::Delegate // ui::StatusIconLinux::Delegate
void OnClick() override; void OnClick() override;
bool HasClickAction() 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 gfx::ImageSkia& GetImage() const override;
const std::u16string& GetToolTip() const override; const std::u16string& GetToolTip() const override;
ui::MenuModel* GetMenuModel() const override; ui::MenuModel* GetMenuModel() const override;
@ -38,10 +38,14 @@ class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate {
private: private:
enum class StatusIconType { enum class StatusIconType {
kDbus, kDbus,
kGtk,
kNone, kNone,
}; };
scoped_refptr<StatusIconLinuxDbus> status_icon_; ui::StatusIconLinux* GetStatusIcon();
scoped_refptr<StatusIconLinuxDbus> status_icon_dbus_;
std::unique_ptr<StatusIconGtk> status_icon_gtk_;
StatusIconType status_icon_type_; StatusIconType status_icon_type_;
gfx::ImageSkia image_; gfx::ImageSkia image_;
@ -51,4 +55,4 @@ class TrayIconGtk : public TrayIcon, public ui::StatusIconLinux::Delegate {
} // namespace electron } // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_GTK_H_ #endif // ELECTRON_SHELL_BROWSER_UI_TRAY_ICON_LINUX_H_