Browse Source

fix: restore original GTK/appindicator implementation of tray icons (#23927)

Co-authored-by: Samuel Attard <[email protected]>
trop[bot] 4 years ago
parent
commit
23bd9166d7

+ 11 - 0
BUILD.gn

@@ -358,6 +358,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",
@@ -549,6 +550,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",
     ]

+ 379 - 0
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 <dlfcn.h>
+#include <gtk/gtk.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#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<app_indicator_new_func>(
+      dlsym(indicator_lib, "app_indicator_new"));
+
+  app_indicator_new_with_path =
+      reinterpret_cast<app_indicator_new_with_path_func>(
+          dlsym(indicator_lib, "app_indicator_new_with_path"));
+
+  app_indicator_set_status = reinterpret_cast<app_indicator_set_status_func>(
+      dlsym(indicator_lib, "app_indicator_set_status"));
+
+  app_indicator_set_attention_icon_full =
+      reinterpret_cast<app_indicator_set_attention_icon_full_func>(
+          dlsym(indicator_lib, "app_indicator_set_attention_icon_full"));
+
+  app_indicator_set_menu = reinterpret_cast<app_indicator_set_menu_func>(
+      dlsym(indicator_lib, "app_indicator_set_menu"));
+
+  app_indicator_set_icon_full =
+      reinterpret_cast<app_indicator_set_icon_full_func>(
+          dlsym(indicator_lib, "app_indicator_set_icon_full"));
+
+  app_indicator_set_icon_theme_path =
+      reinterpret_cast<app_indicator_set_icon_theme_path_func>(
+          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<unsigned char> png_data;
+  if (!gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &png_data))
+    return false;
+  int bytes_written = base::WriteFile(
+      path, reinterpret_cast<char*>(&png_data[0]), png_data.size());
+  return (bytes_written == static_cast<int>(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<base::Environment> 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<unsigned char> 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<char*>(&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<AppIndicatorIconMenu>(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

+ 116 - 0
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 <memory>
+#include <string>
+
+#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<AppIndicatorIconMenu> menu_;
+  ui::MenuModel* menu_model_;
+
+  base::FilePath temp_dir_;
+  int icon_change_count_;
+
+  base::WeakPtrFactory<AppIndicatorIcon> weak_factory_{this};
+
+  DISALLOW_COPY_AND_ASSIGN(AppIndicatorIcon);
+};
+
+}  // namespace gtkui
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_

+ 123 - 0
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 <gtk/gtk.h>
+
+#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

+ 75 - 0
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_

+ 86 - 0
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 <gtk/gtk.h>
+
+#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<AppIndicatorIconMenu>(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

+ 65 - 0
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 <memory>
+
+#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<AppIndicatorIconMenu> menu_;
+
+  DISALLOW_COPY_AND_ASSIGN(GtkStatusIcon);
+};
+
+}  // namespace gtkui
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_UI_GTK_GTK_STATUS_ICON_H_

+ 319 - 0
shell/browser/ui/gtk/menu_util.cc

@@ -0,0 +1,319 @@
+// 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 <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gtk/gtk.h>
+
+#include <map>
+#include <string>
+
+#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/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<GdkModifierType>(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<ui::MenuModel*>(
+      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<int, GtkWidget*> radio_groups;
+  GtkWidget* menu_item = nullptr;
+  for (int i = 0; i < model->GetItemCount(); ++i) {
+    gfx::Image icon;
+    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: {
+        if (model->GetIconAt(i, &icon))
+          menu_item = BuildMenuItemWithImage(label, icon);
+        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<bool*>(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)) {
+          gfx::Image icon;
+          if (model->GetIconAt(id, &icon)) {
+            GdkPixbuf* pixbuf =
+                gtk_util::GdkPixbufFromSkBitmap(*icon.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

+ 63 - 0
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 <gtk/gtk.h>
+
+#include <string>
+
+#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_

+ 62 - 0
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 <gtk/gtk.h>
+
+#include <memory>
+
+#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<views::StatusIconLinux> 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<views::StatusIconLinux>(new AppIndicatorIcon(
+        base::StringPrintf("%s%d", id_prefix, indicators_count), image,
+        tool_tip));
+  } else {
+    return std::unique_ptr<views::StatusIconLinux>(
+        new GtkStatusIcon(image, tool_tip));
+  }
+  return nullptr;
+#endif
+}
+
+}  // namespace gtkui
+
+}  // namespace electron

+ 32 - 0
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 <memory>
+
+#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<views::StatusIconLinux> 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_

+ 4 - 29
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<double>(size.height());
-    size = gfx::Size(static_cast<int>(ratio * size.width()),
-                     static_cast<int>(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<StatusIconLinuxDbus>();
-  icon_->SetIcon(image_);
-  icon_->SetToolTip(tool_tip_);
+  icon_ = gtkui::CreateLinuxStatusIcon(image_, tool_tip_,
+                                       Browser::Get()->GetName().c_str());
   icon_->SetDelegate(this);
 }
 

+ 1 - 2
shell/browser/ui/tray_icon_gtk.h

@@ -8,7 +8,6 @@
 #include <memory>
 #include <string>
 
-#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<StatusIconLinuxDbus> icon_;
+  std::unique_ptr<views::StatusIconLinux> icon_;
   gfx::ImageSkia image_;
   base::string16 tool_tip_;
   ui::MenuModel* menu_model_;