feat: allow GUID parameter to avoid systray demotion on Windows (#21891)

* fix: systray icon demotion

Adding support for GUID parameter in Tray API.
In combination with signed binaries this allows to maintain
the position in the systray on Windows.

* unit tests

* make mac and linux compile
This commit is contained in:
bitdisaster 2020-01-30 21:37:03 -08:00 committed by GitHub
parent 2955c67c4e
commit 89eb309d0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 143 additions and 15 deletions

View file

@ -58,9 +58,10 @@ If you want to keep exact same behaviors on all platforms, you should not
rely on the `click` event and always attach a context menu to the tray icon.
### `new Tray(image)`
### `new Tray(image, [guid])`
* `image` ([NativeImage](native-image.md) | String)
* `guid` String (optional) _Windows_ - Assigns a GUID to the tray icon. If the executable is signed and the signature contains an organization in the subject line then the GUID is permanently associated with that signature. OS level settings like the position of the tray icon in the system tray will persist even if the path to the executable changes. If the executable is not code-signed then the GUID is permanently associated with the path to the executable. Changing the path to the executable will break the creation of the tray icon and a new GUID must be used. However, it is highly recommended to use the GUID parameter only in conjunction with code-signed executable. If an App defines multiple tray icons then each icon must use a separate GUID.
Creates a new tray icon associated with the `image`.

View file

@ -11,6 +11,7 @@
#include "shell/browser/browser.h"
#include "shell/common/api/atom_api_native_image.h"
#include "shell/common/gin_converters/gfx_converter.h"
#include "shell/common/gin_converters/guid_converter.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
@ -54,8 +55,10 @@ namespace electron {
namespace api {
Tray::Tray(gin::Handle<NativeImage> image, gin_helper::Arguments* args)
: tray_icon_(TrayIcon::Create()) {
Tray::Tray(gin::Handle<NativeImage> image,
base::Optional<UUID> guid,
gin_helper::Arguments* args)
: tray_icon_(TrayIcon::Create(guid)) {
SetImage(args->isolate(), image);
tray_icon_->AddObserver(this);
@ -67,12 +70,21 @@ Tray::~Tray() = default;
// static
gin_helper::WrappableBase* Tray::New(gin_helper::ErrorThrower thrower,
gin::Handle<NativeImage> image,
base::Optional<UUID> guid,
gin_helper::Arguments* args) {
if (!Browser::Get()->is_ready()) {
thrower.ThrowError("Cannot create Tray before app is ready");
return nullptr;
}
return new Tray(image, args);
#if defined(OS_WIN)
if (!guid.has_value() && args->Length() > 1) {
thrower.ThrowError("Invalid GUID format");
return nullptr;
}
#endif
return new Tray(image, guid, args);
}
void Tray::OnClicked(const gfx::Rect& bounds,

View file

@ -12,6 +12,7 @@
#include "gin/handle.h"
#include "shell/browser/ui/tray_icon.h"
#include "shell/browser/ui/tray_icon_observer.h"
#include "shell/common/gin_converters/guid_converter.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/trackable_object.h"
@ -36,13 +37,16 @@ class Tray : public gin_helper::TrackableObject<Tray>, public TrayIconObserver {
public:
static gin_helper::WrappableBase* New(gin_helper::ErrorThrower thrower,
gin::Handle<NativeImage> image,
base::Optional<UUID> guid,
gin_helper::Arguments* args);
static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype);
protected:
Tray(gin::Handle<NativeImage> image, gin_helper::Arguments* args);
Tray(gin::Handle<NativeImage> image,
base::Optional<UUID> guid,
gin_helper::Arguments* args);
~Tray() override;
// TrayIconObserver:

View file

@ -11,13 +11,14 @@
#include "base/observer_list.h"
#include "shell/browser/ui/atom_menu_model.h"
#include "shell/browser/ui/tray_icon_observer.h"
#include "shell/common/gin_converters/guid_converter.h"
#include "ui/gfx/geometry/rect.h"
namespace electron {
class TrayIcon {
public:
static TrayIcon* Create();
static TrayIcon* Create(base::Optional<UUID> guid);
#if defined(OS_WIN)
using ImageType = HICON;

View file

@ -341,7 +341,7 @@ gfx::Rect TrayIconCocoa::GetBounds() {
}
// static
TrayIcon* TrayIcon::Create() {
TrayIcon* TrayIcon::Create(base::Optional<UUID> guid) {
return new TrayIconCocoa;
}

View file

@ -91,7 +91,7 @@ bool TrayIconGtk::HasClickAction() {
}
// static
TrayIcon* TrayIcon::Create() {
TrayIcon* TrayIcon::Create(base::Optional<UUID> guid) {
return new TrayIconGtk;
}

View file

@ -8,9 +8,9 @@
namespace electron {
// static
TrayIcon* TrayIcon::Create() {
TrayIcon* TrayIcon::Create(base::Optional<UUID> guid) {
static NotifyIconHost host;
return host.CreateNotifyIcon();
return host.CreateNotifyIcon(guid);
}
} // namespace electron

View file

@ -4,6 +4,7 @@
#include "shell/browser/ui/win/notify_icon.h"
#include <objbase.h>
#include <utility>
#include "base/strings/string_number_conversions.h"
@ -43,12 +44,18 @@ UINT ConvertIconType(electron::TrayIcon::IconType type) {
namespace electron {
NotifyIcon::NotifyIcon(NotifyIconHost* host, UINT id, HWND window, UINT message)
NotifyIcon::NotifyIcon(NotifyIconHost* host,
UINT id,
HWND window,
UINT message,
GUID guid)
: host_(host),
icon_id_(id),
window_(window),
message_id_(message),
weak_factory_(this) {
guid_ = guid;
is_using_guid_ = guid != GUID_DEFAULT;
NOTIFYICONDATA icon_data;
InitIconData(&icon_data);
icon_data.uFlags |= NIF_MESSAGE;
@ -246,6 +253,9 @@ gfx::Rect NotifyIcon::GetBounds() {
icon_id.uID = icon_id_;
icon_id.hWnd = window_;
icon_id.cbSize = sizeof(NOTIFYICONIDENTIFIER);
if (is_using_guid_) {
icon_id.guidItem = guid_;
}
RECT rect = {0};
Shell_NotifyIconGetRect(&icon_id, &rect);
@ -257,6 +267,10 @@ void NotifyIcon::InitIconData(NOTIFYICONDATA* icon_data) {
icon_data->cbSize = sizeof(NOTIFYICONDATA);
icon_data->hWnd = window_;
icon_data->uID = icon_id_;
if (is_using_guid_) {
icon_data->uFlags = NIF_GUID;
icon_data->guidItem = guid_;
}
}
void NotifyIcon::OnContextMenuClosed() {

View file

@ -17,6 +17,7 @@
#include "base/memory/weak_ptr.h"
#include "base/win/scoped_gdi_object.h"
#include "shell/browser/ui/tray_icon.h"
#include "shell/browser/ui/win/notify_icon_host.h"
namespace gfx {
class Point;
@ -34,7 +35,11 @@ class NotifyIconHost;
class NotifyIcon : public TrayIcon {
public:
// Constructor which provides this icon's unique ID and messaging window.
NotifyIcon(NotifyIconHost* host, UINT id, HWND window, UINT message);
NotifyIcon(NotifyIconHost* host,
UINT id,
HWND window,
UINT message,
GUID guid);
~NotifyIcon() override;
// Handles a click event from the user - if |left_button_click| is true and
@ -53,6 +58,7 @@ class NotifyIcon : public TrayIcon {
UINT icon_id() const { return icon_id_; }
HWND window() const { return window_; }
UINT message_id() const { return message_id_; }
GUID guid() const { return guid_; }
// Overridden from TrayIcon:
void SetImage(HICON image) override;
@ -89,6 +95,12 @@ class NotifyIcon : public TrayIcon {
// The context menu.
AtomMenuModel* menu_model_ = nullptr;
// An optional GUID used for identifying tray entries on Windows
GUID guid_ = GUID_DEFAULT;
// indicates whether the tray entry is associated with a guid
bool is_using_guid_ = false;
// Context menu associated with this icon (if any).
std::unique_ptr<views::MenuRunner> menu_runner_;

View file

@ -83,9 +83,22 @@ NotifyIconHost::~NotifyIconHost() {
delete ptr;
}
NotifyIcon* NotifyIconHost::CreateNotifyIcon() {
NotifyIcon* NotifyIconHost::CreateNotifyIcon(base::Optional<UUID> guid) {
if (guid.has_value()) {
for (NotifyIcons::const_iterator i(notify_icons_.begin());
i != notify_icons_.end(); ++i) {
NotifyIcon* current_win_icon = static_cast<NotifyIcon*>(*i);
if (current_win_icon->guid() == guid.value()) {
LOG(WARNING)
<< "Guid already in use. Existing tray entry will be replaced.";
}
}
}
NotifyIcon* notify_icon =
new NotifyIcon(this, NextIconId(), window_, kNotifyIconMessage);
new NotifyIcon(this, NextIconId(), window_, kNotifyIconMessage,
guid.has_value() ? guid.value() : GUID_DEFAULT);
notify_icons_.push_back(notify_icon);
return notify_icon;
}

View file

@ -10,6 +10,10 @@
#include <vector>
#include "base/macros.h"
#include "base/optional.h"
#include "shell/common/gin_converters/guid_converter.h"
const GUID GUID_DEFAULT = {0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}};
namespace electron {
@ -20,7 +24,7 @@ class NotifyIconHost {
NotifyIconHost();
~NotifyIconHost();
NotifyIcon* CreateNotifyIcon();
NotifyIcon* CreateNotifyIcon(base::Optional<UUID> guid);
void Remove(NotifyIcon* notify_icon);
private:

View file

@ -0,0 +1,55 @@
// Copyright (c) 2020 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_COMMON_GIN_CONVERTERS_GUID_CONVERTER_H_
#define SHELL_COMMON_GIN_CONVERTERS_GUID_CONVERTER_H_
#if defined(OS_WIN)
#include <rpc.h>
#endif
#include <string>
#include "gin/converter.h"
#if defined(OS_WIN)
typedef GUID UUID;
#else
typedef struct {
} UUID;
#endif
namespace gin {
template <>
struct Converter<UUID> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
UUID* out) {
#if defined(OS_WIN)
std::string guid;
if (!gin::ConvertFromV8(isolate, val, &guid))
return false;
UUID uid;
if (guid.length() > 0) {
unsigned char* uid_cstr = (unsigned char*)guid.c_str();
RPC_STATUS result = UuidFromStringA(uid_cstr, &uid);
if (result == RPC_S_INVALID_STRING_UUID) {
return false;
} else {
*out = uid;
return true;
}
}
return false;
#else
return false;
#endif
}
};
} // namespace gin
#endif // SHELL_COMMON_GIN_CONVERTERS_GUID_CONVERTER_H_

View file

@ -20,6 +20,18 @@ describe('tray module', () => {
tray = new Tray(badPath)
}).to.throw(/Image could not be created from .*/)
})
ifit(process.platform === 'win32')('throws a descriptive error if an invlaid guid is given', () => {
expect(() => {
tray = new Tray(nativeImage.createEmpty(), 'I am not a guid')
}).to.throw('Invalid GUID format')
})
ifit(process.platform === 'win32')('accepts a valid guid', () => {
expect(() => {
tray = new Tray(nativeImage.createEmpty(), '0019A433-3526-48BA-A66C-676742C0FEFB')
}).to.not.throw()
})
})
ifdescribe(process.platform === 'darwin')('tray get/set ignoreDoubleClickEvents', () => {