From d8594f7a782418d2da3351541b170d047392277b Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Tue, 2 Jun 2020 17:00:20 -0700 Subject: [PATCH] fix: restore original GTK/appindicator implementation of tray icons (#23674) --- BUILD.gn | 11 + shell/browser/ui/gtk/app_indicator_icon.cc | 379 ++++++++++++++++++ shell/browser/ui/gtk/app_indicator_icon.h | 116 ++++++ .../browser/ui/gtk/app_indicator_icon_menu.cc | 123 ++++++ .../browser/ui/gtk/app_indicator_icon_menu.h | 75 ++++ shell/browser/ui/gtk/gtk_status_icon.cc | 86 ++++ shell/browser/ui/gtk/gtk_status_icon.h | 65 +++ shell/browser/ui/gtk/menu_util.cc | 320 +++++++++++++++ shell/browser/ui/gtk/menu_util.h | 63 +++ shell/browser/ui/gtk/status_icon.cc | 62 +++ shell/browser/ui/gtk/status_icon.h | 32 ++ shell/browser/ui/tray_icon_gtk.cc | 33 +- shell/browser/ui/tray_icon_gtk.h | 3 +- 13 files changed, 1337 insertions(+), 31 deletions(-) create mode 100644 shell/browser/ui/gtk/app_indicator_icon.cc create mode 100644 shell/browser/ui/gtk/app_indicator_icon.h create mode 100644 shell/browser/ui/gtk/app_indicator_icon_menu.cc create mode 100644 shell/browser/ui/gtk/app_indicator_icon_menu.h create mode 100644 shell/browser/ui/gtk/gtk_status_icon.cc create mode 100644 shell/browser/ui/gtk/gtk_status_icon.h create mode 100644 shell/browser/ui/gtk/menu_util.cc create mode 100644 shell/browser/ui/gtk/menu_util.h create mode 100644 shell/browser/ui/gtk/status_icon.cc create mode 100644 shell/browser/ui/gtk/status_icon.h diff --git a/BUILD.gn b/BUILD.gn index 999c1c568f..d7a4e85fb9 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -322,6 +322,7 @@ source_set("electron_lib") { "shell/common/api:mojo", "//base:base_static", "//base/allocator:buildflags", + "//chrome/app:command_ids", "//chrome/app/resources:platform_locale_settings", "//chrome/services/printing/public/mojom", "//components/certificate_transparency", @@ -523,6 +524,16 @@ source_set("electron_lib") { sources += filenames.lib_sources_nss sources += [ + "shell/browser/ui/gtk/app_indicator_icon.cc", + "shell/browser/ui/gtk/app_indicator_icon.h", + "shell/browser/ui/gtk/app_indicator_icon_menu.cc", + "shell/browser/ui/gtk/app_indicator_icon_menu.h", + "shell/browser/ui/gtk/gtk_status_icon.cc", + "shell/browser/ui/gtk/gtk_status_icon.h", + "shell/browser/ui/gtk/menu_util.cc", + "shell/browser/ui/gtk/menu_util.h", + "shell/browser/ui/gtk/status_icon.cc", + "shell/browser/ui/gtk/status_icon.h", "shell/browser/ui/gtk_util.cc", "shell/browser/ui/gtk_util.h", ] diff --git a/shell/browser/ui/gtk/app_indicator_icon.cc b/shell/browser/ui/gtk/app_indicator_icon.cc new file mode 100644 index 0000000000..b2af6b53e2 --- /dev/null +++ b/shell/browser/ui/gtk/app_indicator_icon.cc @@ -0,0 +1,379 @@ +// Copyright 2013 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/app_indicator_icon.h" + +#include +#include + +#include +#include +#include + +#include "base/bind.h" +#include "base/environment.h" +#include "base/files/file_util.h" +#include "base/hash/md5.h" +#include "base/memory/ref_counted_memory.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/post_task.h" +#include "content/public/browser/browser_thread.h" +#include "shell/browser/ui/gtk/app_indicator_icon_menu.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkCanvas.h" +#include "ui/base/models/menu_model.h" +#include "ui/gfx/codec/png_codec.h" +#include "ui/gfx/image/image.h" +#include "ui/gfx/image/image_skia.h" + +namespace { + +typedef enum { + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + APP_INDICATOR_CATEGORY_COMMUNICATIONS, + APP_INDICATOR_CATEGORY_SYSTEM_SERVICES, + APP_INDICATOR_CATEGORY_HARDWARE, + APP_INDICATOR_CATEGORY_OTHER +} AppIndicatorCategory; + +typedef enum { + APP_INDICATOR_STATUS_PASSIVE, + APP_INDICATOR_STATUS_ACTIVE, + APP_INDICATOR_STATUS_ATTENTION +} AppIndicatorStatus; + +typedef AppIndicator* (*app_indicator_new_func)(const gchar* id, + const gchar* icon_name, + AppIndicatorCategory category); + +typedef AppIndicator* (*app_indicator_new_with_path_func)( + const gchar* id, + const gchar* icon_name, + AppIndicatorCategory category, + const gchar* icon_theme_path); + +typedef void (*app_indicator_set_status_func)(AppIndicator* self, + AppIndicatorStatus status); + +typedef void (*app_indicator_set_attention_icon_full_func)( + AppIndicator* self, + const gchar* icon_name, + const gchar* icon_desc); + +typedef void (*app_indicator_set_menu_func)(AppIndicator* self, GtkMenu* menu); + +typedef void (*app_indicator_set_icon_full_func)(AppIndicator* self, + const gchar* icon_name, + const gchar* icon_desc); + +typedef void (*app_indicator_set_icon_theme_path_func)( + AppIndicator* self, + const gchar* icon_theme_path); + +bool g_attempted_load = false; +bool g_opened = false; + +// Retrieved functions from libappindicator. +app_indicator_new_func app_indicator_new = nullptr; +app_indicator_new_with_path_func app_indicator_new_with_path = nullptr; +app_indicator_set_status_func app_indicator_set_status = nullptr; +app_indicator_set_attention_icon_full_func + app_indicator_set_attention_icon_full = nullptr; +app_indicator_set_menu_func app_indicator_set_menu = nullptr; +app_indicator_set_icon_full_func app_indicator_set_icon_full = nullptr; +app_indicator_set_icon_theme_path_func app_indicator_set_icon_theme_path = + nullptr; + +void EnsureLibAppIndicatorLoaded() { + if (g_attempted_load) + return; + + g_attempted_load = true; + + std::string lib_name = + "libappindicator" + base::NumberToString(GTK_MAJOR_VERSION) + ".so"; + void* indicator_lib = dlopen(lib_name.c_str(), RTLD_LAZY); + + if (!indicator_lib) { + lib_name += ".1"; + indicator_lib = dlopen(lib_name.c_str(), RTLD_LAZY); + } + + if (!indicator_lib) + return; + + g_opened = true; + + app_indicator_new = reinterpret_cast( + dlsym(indicator_lib, "app_indicator_new")); + + app_indicator_new_with_path = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_new_with_path")); + + app_indicator_set_status = reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_status")); + + app_indicator_set_attention_icon_full = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_attention_icon_full")); + + app_indicator_set_menu = reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_menu")); + + app_indicator_set_icon_full = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_icon_full")); + + app_indicator_set_icon_theme_path = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_icon_theme_path")); +} + +// Writes |bitmap| to a file at |path|. Returns true if successful. +bool WriteFile(const base::FilePath& path, const SkBitmap& bitmap) { + std::vector png_data; + if (!gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &png_data)) + return false; + int bytes_written = base::WriteFile( + path, reinterpret_cast(&png_data[0]), png_data.size()); + return (bytes_written == static_cast(png_data.size())); +} + +void DeleteTempDirectory(const base::FilePath& dir_path) { + if (dir_path.empty()) + return; + base::DeleteFile(dir_path, true); +} + +} // namespace + +namespace electron { + +namespace gtkui { + +AppIndicatorIcon::AppIndicatorIcon(std::string id, + const gfx::ImageSkia& image, + const base::string16& tool_tip) + : id_(id), icon_(nullptr), menu_model_(nullptr), icon_change_count_(0) { + std::unique_ptr env(base::Environment::Create()); + desktop_env_ = base::nix::GetDesktopEnvironment(env.get()); + + EnsureLibAppIndicatorLoaded(); + tool_tip_ = base::UTF16ToUTF8(tool_tip); + SetIcon(image); +} +AppIndicatorIcon::~AppIndicatorIcon() { + if (icon_) { + app_indicator_set_status(icon_, APP_INDICATOR_STATUS_PASSIVE); + g_object_unref(icon_); + base::PostTask( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(&DeleteTempDirectory, temp_dir_)); + } +} + +// static +bool AppIndicatorIcon::CouldOpen() { + EnsureLibAppIndicatorLoaded(); + return g_opened; +} + +void AppIndicatorIcon::SetIcon(const gfx::ImageSkia& image) { + if (!g_opened) + return; + + ++icon_change_count_; + + // Copy the bitmap because it may be freed by the time it's accessed in + // another thread. + SkBitmap safe_bitmap = *image.bitmap(); + + const base::TaskTraits kTraits = { + base::ThreadPool(), base::MayBlock(), base::TaskPriority::USER_VISIBLE, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}; + + if (desktop_env_ == base::nix::DESKTOP_ENVIRONMENT_KDE4 || + desktop_env_ == base::nix::DESKTOP_ENVIRONMENT_KDE5) { + base::PostTaskAndReplyWithResult( + FROM_HERE, kTraits, + base::BindOnce(AppIndicatorIcon::WriteKDE4TempImageOnWorkerThread, + safe_bitmap, temp_dir_), + base::BindOnce(&AppIndicatorIcon::SetImageFromFile, + weak_factory_.GetWeakPtr())); + } else { + base::PostTaskAndReplyWithResult( + FROM_HERE, kTraits, + base::BindOnce(AppIndicatorIcon::WriteUnityTempImageOnWorkerThread, + safe_bitmap, icon_change_count_, id_), + base::BindOnce(&AppIndicatorIcon::SetImageFromFile, + weak_factory_.GetWeakPtr())); + } +} + +void AppIndicatorIcon::SetToolTip(const base::string16& tool_tip) { + DCHECK(!tool_tip_.empty()); + tool_tip_ = base::UTF16ToUTF8(tool_tip); + UpdateClickActionReplacementMenuItem(); +} + +void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel* model) { + if (!g_opened) + return; + + menu_model_ = model; + + // The icon is created asynchronously so it might not exist when the menu is + // set. + if (icon_) + SetMenu(); +} + +void AppIndicatorIcon::RefreshPlatformContextMenu() { + menu_->Refresh(); +} + +// static +AppIndicatorIcon::SetImageFromFileParams +AppIndicatorIcon::WriteKDE4TempImageOnWorkerThread( + const SkBitmap& bitmap, + const base::FilePath& existing_temp_dir) { + base::FilePath temp_dir = existing_temp_dir; + if (temp_dir.empty() && + !base::CreateNewTempDirectory(base::FilePath::StringType(), &temp_dir)) { + LOG(WARNING) << "Could not create temporary directory"; + return SetImageFromFileParams(); + } + + base::FilePath icon_theme_path = temp_dir.AppendASCII("icons"); + + // On KDE4, an image located in a directory ending with + // "icons/hicolor/22x22/apps" can be used as the app indicator image because + // "/usr/share/icons/hicolor/22x22/apps" exists. + base::FilePath image_dir = + icon_theme_path.AppendASCII("hicolor").AppendASCII("22x22").AppendASCII( + "apps"); + + if (!base::CreateDirectory(image_dir)) + return SetImageFromFileParams(); + + // On KDE4, the name of the image file for each different looking bitmap must + // be unique. It must also be unique across runs of Chrome. + std::vector bitmap_png_data; + if (!gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &bitmap_png_data)) { + LOG(WARNING) << "Could not encode icon"; + return SetImageFromFileParams(); + } + base::MD5Digest digest; + base::MD5Sum(reinterpret_cast(&bitmap_png_data[0]), + bitmap_png_data.size(), &digest); + std::string icon_name = base::StringPrintf( + "electron_app_indicator2_%s", base::MD5DigestToBase16(digest).c_str()); + + // If |bitmap| is smaller than 22x22, KDE does some really ugly resizing. + // Pad |bitmap| with transparent pixels to make it 22x22. + const int kMinimalSize = 22; + SkBitmap scaled_bitmap; + scaled_bitmap.allocN32Pixels(std::max(bitmap.width(), kMinimalSize), + std::max(bitmap.height(), kMinimalSize)); + scaled_bitmap.eraseARGB(0, 0, 0, 0); + SkCanvas canvas(scaled_bitmap); + canvas.drawBitmap(bitmap, (scaled_bitmap.width() - bitmap.width()) / 2, + (scaled_bitmap.height() - bitmap.height()) / 2); + + base::FilePath image_path = image_dir.Append(icon_name + ".png"); + if (!WriteFile(image_path, scaled_bitmap)) + return SetImageFromFileParams(); + + SetImageFromFileParams params; + params.parent_temp_dir = temp_dir; + params.icon_theme_path = icon_theme_path.value(); + params.icon_name = icon_name; + return params; +} + +// static +AppIndicatorIcon::SetImageFromFileParams +AppIndicatorIcon::WriteUnityTempImageOnWorkerThread(const SkBitmap& bitmap, + int icon_change_count, + const std::string& id) { + // Create a new temporary directory for each image on Unity since using a + // single temporary directory seems to have issues when changing icons in + // quick succession. + base::FilePath temp_dir; + if (!base::CreateNewTempDirectory(base::FilePath::StringType(), &temp_dir)) { + LOG(WARNING) << "Could not create temporary directory"; + return SetImageFromFileParams(); + } + + std::string icon_name = + base::StringPrintf("%s_%d", id.c_str(), icon_change_count); + base::FilePath image_path = temp_dir.Append(icon_name + ".png"); + SetImageFromFileParams params; + if (WriteFile(image_path, bitmap)) { + params.parent_temp_dir = temp_dir; + params.icon_theme_path = temp_dir.value(); + params.icon_name = icon_name; + } + return params; +} + +void AppIndicatorIcon::SetImageFromFile(const SetImageFromFileParams& params) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (params.icon_theme_path.empty()) + return; + + if (!icon_) { + icon_ = + app_indicator_new_with_path(id_.c_str(), params.icon_name.c_str(), + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + params.icon_theme_path.c_str()); + app_indicator_set_status(icon_, APP_INDICATOR_STATUS_ACTIVE); + SetMenu(); + } else { + app_indicator_set_icon_theme_path(icon_, params.icon_theme_path.c_str()); + app_indicator_set_icon_full(icon_, params.icon_name.c_str(), "icon"); + } + + if (temp_dir_ != params.parent_temp_dir) { + base::PostTask( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(&DeleteTempDirectory, temp_dir_)); + temp_dir_ = params.parent_temp_dir; + } +} + +void AppIndicatorIcon::SetMenu() { + menu_ = std::make_unique(menu_model_); + UpdateClickActionReplacementMenuItem(); + app_indicator_set_menu(icon_, menu_->GetGtkMenu()); +} + +void AppIndicatorIcon::UpdateClickActionReplacementMenuItem() { + // The menu may not have been created yet. + if (!menu_.get()) + return; + + if (!delegate()->HasClickAction() && menu_model_) + return; + + DCHECK(!tool_tip_.empty()); + menu_->UpdateClickActionReplacementMenuItem( + tool_tip_.c_str(), + base::Bind(&AppIndicatorIcon::OnClickActionReplacementMenuItemActivated, + base::Unretained(this))); +} + +void AppIndicatorIcon::OnClickActionReplacementMenuItemActivated() { + if (delegate()) + delegate()->OnClick(); +} + +} // namespace gtkui + +} // namespace electron diff --git a/shell/browser/ui/gtk/app_indicator_icon.h b/shell/browser/ui/gtk/app_indicator_icon.h new file mode 100644 index 0000000000..4cf1951187 --- /dev/null +++ b/shell/browser/ui/gtk/app_indicator_icon.h @@ -0,0 +1,116 @@ +// Copyright 2013 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 SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_ +#define SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_ + +#include +#include + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/nix/xdg_util.h" +#include "ui/base/glib/glib_signal.h" +#include "ui/views/linux_ui/status_icon_linux.h" + +typedef struct _AppIndicator AppIndicator; +typedef struct _GtkWidget GtkWidget; + +class SkBitmap; + +namespace gfx { +class ImageSkia; +} + +namespace ui { +class MenuModel; +} + +namespace electron { + +namespace gtkui { + +class AppIndicatorIconMenu; + +// Status icon implementation which uses libappindicator. +class AppIndicatorIcon : public views::StatusIconLinux { + public: + // The id uniquely identifies the new status icon from other chrome status + // icons. + AppIndicatorIcon(std::string id, + const gfx::ImageSkia& image, + const base::string16& tool_tip); + ~AppIndicatorIcon() override; + + // Indicates whether libappindicator so could be opened. + static bool CouldOpen(); + + // Overridden from views::StatusIconLinux: + void SetIcon(const gfx::ImageSkia& image) override; + void SetToolTip(const base::string16& tool_tip) override; + void UpdatePlatformContextMenu(ui::MenuModel* menu) override; + void RefreshPlatformContextMenu() override; + + private: + struct SetImageFromFileParams { + // The temporary directory in which the icon(s) were written. + base::FilePath parent_temp_dir; + + // The icon theme path to pass to libappindicator. + std::string icon_theme_path; + + // The icon name to pass to libappindicator. + std::string icon_name; + }; + + // Writes |bitmap| to a temporary directory on a worker thread. The temporary + // directory is selected based on KDE's quirks. + static SetImageFromFileParams WriteKDE4TempImageOnWorkerThread( + const SkBitmap& bitmap, + const base::FilePath& existing_temp_dir); + + // Writes |bitmap| to a temporary directory on a worker thread. The temporary + // directory is selected based on Unity's quirks. + static SetImageFromFileParams WriteUnityTempImageOnWorkerThread( + const SkBitmap& bitmap, + int icon_change_count, + const std::string& id); + + void SetImageFromFile(const SetImageFromFileParams& params); + void SetMenu(); + + // Sets a menu item at the top of the menu as a replacement for the status + // icon click action. Clicking on this menu item should simulate a status icon + // click by despatching a click event. + void UpdateClickActionReplacementMenuItem(); + + // Callback for when the status icon click replacement menu item is activated. + void OnClickActionReplacementMenuItemActivated(); + + std::string id_; + std::string tool_tip_; + + // Used to select KDE or Unity for image setting. + base::nix::DesktopEnvironment desktop_env_; + + // Gtk status icon wrapper + AppIndicator* icon_; + + std::unique_ptr menu_; + ui::MenuModel* menu_model_; + + base::FilePath temp_dir_; + int icon_change_count_; + + base::WeakPtrFactory weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(AppIndicatorIcon); +}; + +} // namespace gtkui + +} // namespace electron + +#endif // SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_ diff --git a/shell/browser/ui/gtk/app_indicator_icon_menu.cc b/shell/browser/ui/gtk/app_indicator_icon_menu.cc new file mode 100644 index 0000000000..e9f9ee3912 --- /dev/null +++ b/shell/browser/ui/gtk/app_indicator_icon_menu.cc @@ -0,0 +1,123 @@ +// 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/app_indicator_icon_menu.h" + +#include + +#include "base/bind.h" +#include "base/debug/leak_annotations.h" +#include "shell/browser/ui/gtk/menu_util.h" +#include "ui/base/models/menu_model.h" + +namespace electron { + +namespace gtkui { + +AppIndicatorIconMenu::AppIndicatorIconMenu(ui::MenuModel* model) + : menu_model_(model), + click_action_replacement_menu_item_added_(false), + gtk_menu_(nullptr), + block_activation_(false) { + { + ANNOTATE_SCOPED_MEMORY_LEAK; // http://crbug.com/378770 + gtk_menu_ = gtk_menu_new(); + } + g_object_ref_sink(gtk_menu_); + if (menu_model_) { + BuildSubmenuFromModel(menu_model_, gtk_menu_, + G_CALLBACK(OnMenuItemActivatedThunk), + &block_activation_, this); + Refresh(); + } +} + +AppIndicatorIconMenu::~AppIndicatorIconMenu() { + gtk_widget_destroy(gtk_menu_); + g_object_unref(gtk_menu_); +} + +void AppIndicatorIconMenu::UpdateClickActionReplacementMenuItem( + const char* label, + const base::Closure& callback) { + click_action_replacement_callback_ = callback; + + if (click_action_replacement_menu_item_added_) { + GList* children = gtk_container_get_children(GTK_CONTAINER(gtk_menu_)); + for (GList* child = children; child; child = g_list_next(child)) { + if (g_object_get_data(G_OBJECT(child->data), "click-action-item") != + nullptr) { + gtk_menu_item_set_label(GTK_MENU_ITEM(child->data), label); + break; + } + } + g_list_free(children); + } else { + click_action_replacement_menu_item_added_ = true; + + // If |menu_model_| is non empty, add a separator to separate the + // "click action replacement menu item" from the other menu items. + if (menu_model_ && menu_model_->GetItemCount() > 0) { + GtkWidget* menu_item = gtk_separator_menu_item_new(); + gtk_widget_show(menu_item); + gtk_menu_shell_prepend(GTK_MENU_SHELL(gtk_menu_), menu_item); + } + + GtkWidget* menu_item = gtk_menu_item_new_with_mnemonic(label); + g_object_set_data(G_OBJECT(menu_item), "click-action-item", + GINT_TO_POINTER(1)); + g_signal_connect(menu_item, "activate", + G_CALLBACK(OnClickActionReplacementMenuItemActivatedThunk), + this); + gtk_widget_show(menu_item); + gtk_menu_shell_prepend(GTK_MENU_SHELL(gtk_menu_), menu_item); + } +} + +void AppIndicatorIconMenu::Refresh() { + gtk_container_foreach(GTK_CONTAINER(gtk_menu_), SetMenuItemInfo, + &block_activation_); +} + +GtkMenu* AppIndicatorIconMenu::GetGtkMenu() { + return GTK_MENU(gtk_menu_); +} + +void AppIndicatorIconMenu::OnClickActionReplacementMenuItemActivated( + GtkWidget* menu_item) { + click_action_replacement_callback_.Run(); +} + +void AppIndicatorIconMenu::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/app_indicator_icon_menu.h b/shell/browser/ui/gtk/app_indicator_icon_menu.h new file mode 100644 index 0000000000..7c0146114f --- /dev/null +++ b/shell/browser/ui/gtk/app_indicator_icon_menu.h @@ -0,0 +1,75 @@ +// 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 SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_MENU_H_ +#define SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_MENU_H_ + +#include "base/callback.h" +#include "base/macros.h" +#include "ui/base/glib/glib_signal.h" + +typedef struct _GtkMenu GtkMenu; +typedef struct _GtkWidget GtkWidget; + +namespace ui { +class MenuModel; +} + +namespace electron { + +namespace gtkui { + +// The app indicator icon's menu. +class AppIndicatorIconMenu { + public: + explicit AppIndicatorIconMenu(ui::MenuModel* model); + virtual ~AppIndicatorIconMenu(); + + // Sets a menu item at the top of |gtk_menu_| as a replacement for the app + // indicator icon's click action. |callback| is called when the menu item + // is activated. + void UpdateClickActionReplacementMenuItem(const char* label, + const base::Closure& callback); + + // Refreshes all the menu item labels and menu item checked/enabled states. + void Refresh(); + + GtkMenu* GetGtkMenu(); + + private: + // Callback for when the "click action replacement" menu item is activated. + CHROMEG_CALLBACK_0(AppIndicatorIconMenu, + void, + OnClickActionReplacementMenuItemActivated, + GtkWidget*); + + // Callback for when a menu item is activated. + CHROMEG_CALLBACK_0(AppIndicatorIconMenu, + void, + OnMenuItemActivated, + GtkWidget*); + + // Not owned. + ui::MenuModel* menu_model_; + + // Whether a "click action replacement" menu item has been added to the menu. + bool click_action_replacement_menu_item_added_; + + // Called when the click action replacement menu item is activated. When a + // menu item from |menu_model_| is activated, MenuModel::ActivatedAt() is + // invoked and is assumed to do any necessary processing. + base::Closure click_action_replacement_callback_; + + GtkWidget* gtk_menu_; + + bool block_activation_; + + DISALLOW_COPY_AND_ASSIGN(AppIndicatorIconMenu); +}; + +} // namespace gtkui + +} // namespace electron + +#endif // SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_MENU_H_ diff --git a/shell/browser/ui/gtk/gtk_status_icon.cc b/shell/browser/ui/gtk/gtk_status_icon.cc new file mode 100644 index 0000000000..244f3876f8 --- /dev/null +++ b/shell/browser/ui/gtk/gtk_status_icon.cc @@ -0,0 +1,86 @@ +// 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/gtk_status_icon.h" + +#include + +#include "base/debug/leak_annotations.h" +#include "base/strings/utf_string_conversions.h" +#include "shell/browser/ui/gtk/app_indicator_icon_menu.h" +#include "shell/browser/ui/gtk_util.h" +#include "ui/base/models/menu_model.h" +#include "ui/gfx/image/image_skia.h" + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + +namespace electron { + +namespace gtkui { + +GtkStatusIcon::GtkStatusIcon(const gfx::ImageSkia& image, + const base::string16& tool_tip) { + GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap(*image.bitmap()); + { + // GTK has a bug that leaks 384 bytes when creating a GtkStatusIcon. It + // will not be fixed since the status icon was deprecated in version 3.14. + // Luckily, Chromium doesn't need to create a status icon very often, if at + // all. + ANNOTATE_SCOPED_MEMORY_LEAK; + gtk_status_icon_ = gtk_status_icon_new_from_pixbuf(pixbuf); + } + g_object_unref(pixbuf); + + g_signal_connect(gtk_status_icon_, "activate", G_CALLBACK(OnClickThunk), + this); + g_signal_connect(gtk_status_icon_, "popup_menu", + G_CALLBACK(OnContextMenuRequestedThunk), this); + SetToolTip(tool_tip); +} + +GtkStatusIcon::~GtkStatusIcon() { + gtk_status_icon_set_visible(gtk_status_icon_, FALSE); + g_object_unref(gtk_status_icon_); +} + +void GtkStatusIcon::SetIcon(const gfx::ImageSkia& image) { + GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap(*image.bitmap()); + gtk_status_icon_set_from_pixbuf(gtk_status_icon_, pixbuf); + g_object_unref(pixbuf); +} + +void GtkStatusIcon::SetToolTip(const base::string16& tool_tip) { + gtk_status_icon_set_tooltip_text(gtk_status_icon_, + base::UTF16ToUTF8(tool_tip).c_str()); +} + +void GtkStatusIcon::UpdatePlatformContextMenu(ui::MenuModel* model) { + menu_.reset(); + if (model) + menu_ = std::make_unique(model); +} + +void GtkStatusIcon::RefreshPlatformContextMenu() { + if (menu_.get()) + menu_->Refresh(); +} + +void GtkStatusIcon::OnClick(GtkStatusIcon* status_icon) { + if (delegate()) + delegate()->OnClick(); +} + +void GtkStatusIcon::OnContextMenuRequested(GtkStatusIcon* status_icon, + guint button, + guint32 activate_time) { + if (menu_.get()) { + gtk_menu_popup(menu_->GetGtkMenu(), nullptr, nullptr, + gtk_status_icon_position_menu, gtk_status_icon_, button, + activate_time); + } +} + +} // namespace gtkui + +} // namespace electron diff --git a/shell/browser/ui/gtk/gtk_status_icon.h b/shell/browser/ui/gtk/gtk_status_icon.h new file mode 100644 index 0000000000..07c49f25f8 --- /dev/null +++ b/shell/browser/ui/gtk/gtk_status_icon.h @@ -0,0 +1,65 @@ +// 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 SHELL_BROWSER_UI_GTK_GTK_STATUS_ICON_H_ +#define SHELL_BROWSER_UI_GTK_GTK_STATUS_ICON_H_ + +#include + +#include "base/macros.h" +#include "base/strings/string16.h" +#include "ui/base/glib/glib_integers.h" +#include "ui/base/glib/glib_signal.h" +#include "ui/views/linux_ui/status_icon_linux.h" + +typedef struct _GtkStatusIcon GtkStatusIcon; + +namespace gfx { +class ImageSkia; +} + +namespace ui { +class MenuModel; +} + +namespace electron { + +namespace gtkui { +class AppIndicatorIconMenu; + +// Status icon implementation which uses the system tray X11 spec (via +// GtkStatusIcon). +class GtkStatusIcon : public views::StatusIconLinux { + public: + GtkStatusIcon(const gfx::ImageSkia& image, const base::string16& tool_tip); + ~GtkStatusIcon() override; + + // Overridden from views::StatusIconLinux: + void SetIcon(const gfx::ImageSkia& image) override; + void SetToolTip(const base::string16& tool_tip) override; + void UpdatePlatformContextMenu(ui::MenuModel* menu) override; + void RefreshPlatformContextMenu() override; + + private: + CHROMEG_CALLBACK_0(GtkStatusIcon, void, OnClick, GtkStatusIcon*); + + CHROMEG_CALLBACK_2(GtkStatusIcon, + void, + OnContextMenuRequested, + GtkStatusIcon*, + guint, + guint); + + ::GtkStatusIcon* gtk_status_icon_; + + std::unique_ptr menu_; + + DISALLOW_COPY_AND_ASSIGN(GtkStatusIcon); +}; + +} // namespace gtkui + +} // namespace electron + +#endif // SHELL_BROWSER_UI_GTK_GTK_STATUS_ICON_H_ diff --git a/shell/browser/ui/gtk/menu_util.cc b/shell/browser/ui/gtk/menu_util.cc new file mode 100644 index 0000000000..3549e71f53 --- /dev/null +++ b/shell/browser/ui/gtk/menu_util.cc @@ -0,0 +1,320 @@ +// Copyright 2013 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_util.h" + +#include +#include +#include + +#include +#include + +#include "base/strings/utf_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" +#include "shell/browser/ui/gtk_util.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkUnPreMultiply.h" +#include "ui/base/accelerators/accelerator.h" +#include "ui/base/accelerators/menu_label_accelerator_util_linux.h" +#include "ui/base/models/image_model.h" +#include "ui/base/models/menu_model.h" +#include "ui/events/event_constants.h" +#include "ui/events/keycodes/keyboard_code_conversion_x.h" + +namespace electron { + +namespace gtkui { + +namespace { + +int EventFlagsFromGdkState(guint state) { + int flags = ui::EF_NONE; + flags |= (state & GDK_SHIFT_MASK) ? ui::EF_SHIFT_DOWN : ui::EF_NONE; + flags |= (state & GDK_LOCK_MASK) ? ui::EF_CAPS_LOCK_ON : ui::EF_NONE; + flags |= (state & GDK_CONTROL_MASK) ? ui::EF_CONTROL_DOWN : ui::EF_NONE; + flags |= (state & GDK_MOD1_MASK) ? ui::EF_ALT_DOWN : ui::EF_NONE; + flags |= (state & GDK_BUTTON1_MASK) ? ui::EF_LEFT_MOUSE_BUTTON : ui::EF_NONE; + flags |= + (state & GDK_BUTTON2_MASK) ? ui::EF_MIDDLE_MOUSE_BUTTON : ui::EF_NONE; + flags |= (state & GDK_BUTTON3_MASK) ? ui::EF_RIGHT_MOUSE_BUTTON : ui::EF_NONE; + return flags; +} + +guint GetGdkKeyCodeForAccelerator(const ui::Accelerator& accelerator) { + // The second parameter is false because accelerator keys are expressed in + // terms of the non-shift-modified key. + return XKeysymForWindowsKeyCode(accelerator.key_code(), false); +} + +GdkModifierType GetGdkModifierForAccelerator( + const ui::Accelerator& accelerator) { + int event_flag = accelerator.modifiers(); + int modifier = 0; + if (event_flag & ui::EF_SHIFT_DOWN) + modifier |= GDK_SHIFT_MASK; + if (event_flag & ui::EF_CONTROL_DOWN) + modifier |= GDK_CONTROL_MASK; + if (event_flag & ui::EF_ALT_DOWN) + modifier |= GDK_MOD1_MASK; + return static_cast(modifier); +} + +} // namespace + +GtkWidget* BuildMenuItemWithImage(const std::string& label, GtkWidget* image) { +// GTK4 removed support for image menu items. +#if GTK_CHECK_VERSION(3, 90, 0) + return gtk_menu_item_new_with_mnemonic(label.c_str()); +#else + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + GtkWidget* menu_item = gtk_image_menu_item_new_with_mnemonic(label.c_str()); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), image); + G_GNUC_END_IGNORE_DEPRECATIONS; + return menu_item; +#endif +} + +GtkWidget* BuildMenuItemWithImage(const std::string& label, + const gfx::Image& icon) { + GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap(*icon.ToSkBitmap()); + + GtkWidget* menu_item = + BuildMenuItemWithImage(label, gtk_image_new_from_pixbuf(pixbuf)); + g_object_unref(pixbuf); + return menu_item; +} + +GtkWidget* BuildMenuItemWithLabel(const std::string& label) { + return gtk_menu_item_new_with_mnemonic(label.c_str()); +} + +ui::MenuModel* ModelForMenuItem(GtkMenuItem* menu_item) { + return reinterpret_cast( + g_object_get_data(G_OBJECT(menu_item), "model")); +} + +GtkWidget* AppendMenuItemToMenu(int index, + ui::MenuModel* model, + GtkWidget* menu_item, + GtkWidget* menu, + bool connect_to_activate, + GCallback item_activated_cb, + void* this_ptr) { + // Set the ID of a menu item. + // Add 1 to the menu_id to avoid setting zero (null) to "menu-id". + g_object_set_data(G_OBJECT(menu_item), "menu-id", GINT_TO_POINTER(index + 1)); + + // Native menu items do their own thing, so only selectively listen for the + // activate signal. + if (connect_to_activate) { + g_signal_connect(menu_item, "activate", item_activated_cb, this_ptr); + } + + // AppendMenuItemToMenu is used both internally when we control menu creation + // from a model (where the model can choose to hide certain menu items), and + // with immediate commands which don't provide the option. + if (model) { + if (model->IsVisibleAt(index)) + gtk_widget_show(menu_item); + } else { + gtk_widget_show(menu_item); + } + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + return menu_item; +} + +bool GetMenuItemID(GtkWidget* menu_item, int* menu_id) { + gpointer id_ptr = g_object_get_data(G_OBJECT(menu_item), "menu-id"); + if (id_ptr != nullptr) { + *menu_id = GPOINTER_TO_INT(id_ptr) - 1; + return true; + } + + return false; +} + +void ExecuteCommand(ui::MenuModel* model, int id) { + GdkEvent* event = gtk_get_current_event(); + int event_flags = 0; + + if (event && event->type == GDK_BUTTON_RELEASE) + event_flags = EventFlagsFromGdkState(event->button.state); + model->ActivatedAt(id, event_flags); + + if (event) + gdk_event_free(event); +} + +void BuildSubmenuFromModel(ui::MenuModel* model, + GtkWidget* menu, + GCallback item_activated_cb, + bool* block_activation, + void* this_ptr) { + std::map radio_groups; + GtkWidget* menu_item = nullptr; + for (int i = 0; i < model->GetItemCount(); ++i) { + std::string label = ui::ConvertAcceleratorsFromWindowsStyle( + base::UTF16ToUTF8(model->GetLabelAt(i))); + + bool connect_to_activate = true; + + switch (model->GetTypeAt(i)) { + case ui::MenuModel::TYPE_SEPARATOR: + menu_item = gtk_separator_menu_item_new(); + break; + + case ui::MenuModel::TYPE_CHECK: + menu_item = gtk_check_menu_item_new_with_mnemonic(label.c_str()); + break; + + case ui::MenuModel::TYPE_RADIO: { + auto iter = radio_groups.find(model->GetGroupIdAt(i)); + + if (iter == radio_groups.end()) { + menu_item = + gtk_radio_menu_item_new_with_mnemonic(nullptr, label.c_str()); + radio_groups[model->GetGroupIdAt(i)] = menu_item; + } else { + menu_item = gtk_radio_menu_item_new_with_mnemonic_from_widget( + GTK_RADIO_MENU_ITEM(iter->second), label.c_str()); + } + break; + } + case ui::MenuModel::TYPE_BUTTON_ITEM: { + NOTIMPLEMENTED(); + break; + } + case ui::MenuModel::TYPE_SUBMENU: + case ui::MenuModel::TYPE_COMMAND: { + auto icon_model = model->GetIconAt(i); + if (!icon_model.IsEmpty()) + menu_item = BuildMenuItemWithImage(label, icon_model.GetImage()); + else + menu_item = BuildMenuItemWithLabel(label); +#if !GTK_CHECK_VERSION(3, 90, 0) + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + if (GTK_IS_IMAGE_MENU_ITEM(menu_item)) { + gtk_image_menu_item_set_always_show_image( + GTK_IMAGE_MENU_ITEM(menu_item), TRUE); + } + G_GNUC_END_IGNORE_DEPRECATIONS; +#endif + break; + } + + default: + NOTREACHED(); + } + + if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU) { + GtkWidget* submenu = gtk_menu_new(); + ui::MenuModel* submenu_model = model->GetSubmenuModelAt(i); + BuildSubmenuFromModel(submenu_model, submenu, item_activated_cb, + block_activation, this_ptr); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); + + // Update all the menu item info in the newly-generated menu. + gtk_container_foreach(GTK_CONTAINER(submenu), SetMenuItemInfo, + block_activation); + submenu_model->MenuWillShow(); + connect_to_activate = false; + } + +#if defined(USE_X11) + ui::Accelerator accelerator; + if (model->GetAcceleratorAt(i, &accelerator)) { + gtk_widget_add_accelerator(menu_item, "activate", nullptr, + GetGdkKeyCodeForAccelerator(accelerator), + GetGdkModifierForAccelerator(accelerator), + GTK_ACCEL_VISIBLE); + } +#endif + + g_object_set_data(G_OBJECT(menu_item), "model", model); + AppendMenuItemToMenu(i, model, menu_item, menu, connect_to_activate, + item_activated_cb, this_ptr); + + menu_item = nullptr; + } +} + +void SetMenuItemInfo(GtkWidget* widget, void* block_activation_ptr) { + if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) { + // We need to explicitly handle this case because otherwise we'll ask the + // menu delegate about something with an invalid id. + return; + } + + int id; + if (!GetMenuItemID(widget, &id)) + return; + + ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(widget)); + if (!model) { + // If we're not providing the sub menu, then there's no model. For + // example, the IME submenu doesn't have a model. + return; + } + bool* block_activation = static_cast(block_activation_ptr); + + if (GTK_IS_CHECK_MENU_ITEM(widget)) { + GtkCheckMenuItem* item = GTK_CHECK_MENU_ITEM(widget); + + // gtk_check_menu_item_set_active() will send the activate signal. Touching + // the underlying "active" property will also call the "activate" handler + // for this menu item. So we prevent the "activate" handler from + // being called while we set the checkbox. + // Why not use one of the glib signal-blocking functions? Because when we + // toggle a radio button, it will deactivate one of the other radio buttons, + // which we don't have a pointer to. + *block_activation = true; + gtk_check_menu_item_set_active(item, model->IsItemCheckedAt(id)); + *block_activation = false; + } + + if (GTK_IS_MENU_ITEM(widget)) { + gtk_widget_set_sensitive(widget, model->IsEnabledAt(id)); + + if (model->IsVisibleAt(id)) { + // Update the menu item label if it is dynamic. + if (model->IsItemDynamicAt(id)) { + std::string label = ui::ConvertAcceleratorsFromWindowsStyle( + base::UTF16ToUTF8(model->GetLabelAt(id))); + + gtk_menu_item_set_label(GTK_MENU_ITEM(widget), label.c_str()); +#if !GTK_CHECK_VERSION(3, 90, 0) + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + if (GTK_IS_IMAGE_MENU_ITEM(widget)) { + auto icon_model = model->GetIconAt(id); + if (!icon_model.IsEmpty()) { + GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap( + *icon_model.GetImage().ToSkBitmap()); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), + gtk_image_new_from_pixbuf(pixbuf)); + g_object_unref(pixbuf); + } else { + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), nullptr); + } + } + G_GNUC_END_IGNORE_DEPRECATIONS; +#endif + } + + gtk_widget_show(widget); + } else { + gtk_widget_hide(widget); + } + + GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget)); + if (submenu) { + gtk_container_foreach(GTK_CONTAINER(submenu), &SetMenuItemInfo, + block_activation_ptr); + } + } +} + +} // namespace gtkui + +} // namespace electron diff --git a/shell/browser/ui/gtk/menu_util.h b/shell/browser/ui/gtk/menu_util.h new file mode 100644 index 0000000000..4dedb7b256 --- /dev/null +++ b/shell/browser/ui/gtk/menu_util.h @@ -0,0 +1,63 @@ +// Copyright 2013 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 SHELL_BROWSER_UI_GTK_MENU_UTIL_H_ +#define SHELL_BROWSER_UI_GTK_MENU_UTIL_H_ + +#include + +#include + +#include "ui/gfx/image/image.h" + +namespace ui { +class MenuModel; +} + +namespace electron { + +namespace gtkui { + +// Builds GtkImageMenuItems. +GtkWidget* BuildMenuItemWithImage(const std::string& label, GtkWidget* image); +GtkWidget* BuildMenuItemWithImage(const std::string& label, + const gfx::Image& icon); +GtkWidget* BuildMenuItemWithLabel(const std::string& label); + +ui::MenuModel* ModelForMenuItem(GtkMenuItem* menu_item); + +// This method is used to build the menu dynamically. The return value is the +// new menu item. +GtkWidget* AppendMenuItemToMenu(int index, + ui::MenuModel* model, + GtkWidget* menu_item, + GtkWidget* menu, + bool connect_to_activate, + GCallback item_activated_cb, + void* this_ptr); + +// Gets the ID of a menu item. +// Returns true if the menu item has an ID. +bool GetMenuItemID(GtkWidget* menu_item, int* menu_id); + +// Execute command associated with specified id. +void ExecuteCommand(ui::MenuModel* model, int id); + +// Creates a GtkMenu from |model_|. block_activation_ptr is used to disable +// the item_activated_callback while we set up the set up the menu items. +// See comments in definition of SetMenuItemInfo for more info. +void BuildSubmenuFromModel(ui::MenuModel* model, + GtkWidget* menu, + GCallback item_activated_cb, + bool* block_activation, + void* this_ptr); + +// Sets the check mark, enabled/disabled state and dynamic labels on menu items. +void SetMenuItemInfo(GtkWidget* widget, void* block_activation_ptr); + +} // namespace gtkui + +} // namespace electron + +#endif // SHELL_BROWSER_UI_GTK_MENU_UTIL_H_ diff --git a/shell/browser/ui/gtk/status_icon.cc b/shell/browser/ui/gtk/status_icon.cc new file mode 100644 index 0000000000..43b91c645e --- /dev/null +++ b/shell/browser/ui/gtk/status_icon.cc @@ -0,0 +1,62 @@ +// Copyright (c) 2020 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// Copyright (c) 2020 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/status_icon.h" + +#include + +#include + +#include "base/strings/stringprintf.h" +#include "shell/browser/ui/gtk/app_indicator_icon.h" +#include "shell/browser/ui/gtk/gtk_status_icon.h" + +namespace electron { + +namespace gtkui { + +namespace { + +int indicators_count = 0; + +} + +bool IsStatusIconSupported() { +#if GTK_CHECK_VERSION(3, 90, 0) + NOTIMPLEMENTED(); + return false; +#else + return true; +#endif +} + +std::unique_ptr CreateLinuxStatusIcon( + const gfx::ImageSkia& image, + const base::string16& tool_tip, + const char* id_prefix) { +#if GTK_CHECK_VERSION(3, 90, 0) + NOTIMPLEMENTED(); + return nullptr; +#else + if (AppIndicatorIcon::CouldOpen()) { + ++indicators_count; + + return std::unique_ptr(new AppIndicatorIcon( + base::StringPrintf("%s%d", id_prefix, indicators_count), image, + tool_tip)); + } else { + return std::unique_ptr( + new GtkStatusIcon(image, tool_tip)); + } + return nullptr; +#endif +} + +} // namespace gtkui + +} // namespace electron diff --git a/shell/browser/ui/gtk/status_icon.h b/shell/browser/ui/gtk/status_icon.h new file mode 100644 index 0000000000..3fd40dcf27 --- /dev/null +++ b/shell/browser/ui/gtk/status_icon.h @@ -0,0 +1,32 @@ +// Copyright (c) 2020 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// Copyright (c) 2020 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 SHELL_BROWSER_UI_GTK_STATUS_ICON_H_ +#define SHELL_BROWSER_UI_GTK_STATUS_ICON_H_ + +#include + +#include "base/strings/string_util.h" +#include "ui/gfx/image/image_skia.h" +#include "ui/views/linux_ui/status_icon_linux.h" + +namespace electron { + +namespace gtkui { + +bool IsStatusIconSupported(); +std::unique_ptr CreateLinuxStatusIcon( + const gfx::ImageSkia& image, + const base::string16& tool_tip, + const char* id_prefix); + +} // namespace gtkui + +} // namespace electron + +#endif // SHELL_BROWSER_UI_GTK_STATUS_ICON_H_ diff --git a/shell/browser/ui/tray_icon_gtk.cc b/shell/browser/ui/tray_icon_gtk.cc index 046c54f9fd..1c97aa61be 100644 --- a/shell/browser/ui/tray_icon_gtk.cc +++ b/shell/browser/ui/tray_icon_gtk.cc @@ -6,44 +6,20 @@ #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" -#include "chrome/browser/ui/views/status_icons/status_icon_linux_dbus.h" #include "shell/browser/browser.h" +#include "shell/browser/ui/gtk/status_icon.h" #include "shell/common/application_info.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia_operations.h" namespace electron { -namespace { - -gfx::ImageSkia GetIconFromImage(const gfx::Image& image) { - auto icon = image.AsImageSkia(); - auto size = icon.size(); - - // Systray icons are historically 22 pixels tall, e.g. on Ubuntu GNOME, - // KDE, and xfce. Taller icons are causing incorrect sizing issues -- e.g. - // a 1x1 icon -- so for now, pin the height manually. Similar behavior to - // https://bugs.chromium.org/p/chromium/issues/detail?id=1042098 ? - static constexpr int DESIRED_HEIGHT = 22; - if ((size.height() != 0) && (size.height() != DESIRED_HEIGHT)) { - const double ratio = DESIRED_HEIGHT / static_cast(size.height()); - size = gfx::Size(static_cast(ratio * size.width()), - static_cast(ratio * size.height())); - icon = gfx::ImageSkiaOperations::CreateResizedImage( - icon, skia::ImageOperations::RESIZE_BEST, size); - } - - return icon; -} - -} // namespace - TrayIconGtk::TrayIconGtk() = default; TrayIconGtk::~TrayIconGtk() = default; void TrayIconGtk::SetImage(const gfx::Image& image) { - image_ = GetIconFromImage(image); + image_ = image.AsImageSkia(); if (icon_) { icon_->SetIcon(image_); @@ -52,9 +28,8 @@ void TrayIconGtk::SetImage(const gfx::Image& image) { tool_tip_ = base::UTF8ToUTF16(GetApplicationName()); - icon_ = base::MakeRefCounted(); - icon_->SetIcon(image_); - icon_->SetToolTip(tool_tip_); + icon_ = gtkui::CreateLinuxStatusIcon(image_, tool_tip_, + Browser::Get()->GetName().c_str()); icon_->SetDelegate(this); } diff --git a/shell/browser/ui/tray_icon_gtk.h b/shell/browser/ui/tray_icon_gtk.h index bf10df377a..1580d60bf5 100644 --- a/shell/browser/ui/tray_icon_gtk.h +++ b/shell/browser/ui/tray_icon_gtk.h @@ -8,7 +8,6 @@ #include #include -#include "chrome/browser/ui/views/status_icons/status_icon_linux_dbus.h" #include "shell/browser/ui/tray_icon.h" #include "ui/views/linux_ui/status_icon_linux.h" @@ -39,7 +38,7 @@ class TrayIconGtk : public TrayIcon, public views::StatusIconLinux::Delegate { void OnImplInitializationFailed() override; private: - scoped_refptr icon_; + std::unique_ptr icon_; gfx::ImageSkia image_; base::string16 tool_tip_; ui::MenuModel* menu_model_;