feat: allow macOS tray to maintain position (#47838)
* feat: allow macOS tray to maintain position * refactor: just use guid * test: fixup tests * docs: clarify UUID format
This commit is contained in:
parent
f49a645c06
commit
a0d983e4b5
13 changed files with 101 additions and 34 deletions
|
@ -79,7 +79,15 @@ app.whenReady().then(() => {
|
|||
### `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.
|
||||
* `guid` string (optional) _Windows_ _macOS_ - A unique string used to identify the tray icon. Must adhere to [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) format.
|
||||
|
||||
**Windows**
|
||||
|
||||
On Windows, 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.
|
||||
|
||||
**MacOS**
|
||||
|
||||
On macOS, the `guid` is a string used to uniquely identify the tray icon and allow it to retain its position between relaunches. Using the same string for a new tray item will create it in the same position as the previous tray item to use the string.
|
||||
|
||||
Creates a new tray icon associated with the `image`.
|
||||
|
||||
|
@ -327,6 +335,10 @@ Returns [`Rectangle`](structures/rectangle.md)
|
|||
|
||||
The `bounds` of this tray icon as `Object`.
|
||||
|
||||
#### `tray.getGUID()` _macOS_ _Windows_
|
||||
|
||||
Returns `string | null` - The GUID used to uniquely identify the tray icon and allow it to retain its position between relaunches, or null if none is set.
|
||||
|
||||
#### `tray.isDestroyed()`
|
||||
|
||||
Returns `boolean` - Whether the tray icon is destroyed.
|
||||
|
|
|
@ -52,10 +52,12 @@ gin::DeprecatedWrapperInfo Tray::kWrapperInfo = {gin::kEmbedderNativeGin};
|
|||
|
||||
Tray::Tray(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<UUID> guid)
|
||||
: tray_icon_(TrayIcon::Create(guid)) {
|
||||
std::optional<base::Uuid> guid)
|
||||
: guid_(guid), tray_icon_(TrayIcon::Create(guid)) {
|
||||
SetImage(isolate, image);
|
||||
tray_icon_->AddObserver(this);
|
||||
if (guid.has_value())
|
||||
tray_icon_->SetAutoSaveName(guid.value().AsLowercaseString());
|
||||
}
|
||||
|
||||
Tray::~Tray() = default;
|
||||
|
@ -63,19 +65,17 @@ Tray::~Tray() = default;
|
|||
// static
|
||||
gin_helper::Handle<Tray> Tray::New(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<UUID> guid,
|
||||
std::optional<base::Uuid> guid,
|
||||
gin::Arguments* args) {
|
||||
if (!Browser::Get()->is_ready()) {
|
||||
thrower.ThrowError("Cannot create Tray before app is ready");
|
||||
return {};
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!guid.has_value() && args->Length() > 1) {
|
||||
thrower.ThrowError("Invalid GUID format");
|
||||
thrower.ThrowError("Invalid GUID format - GUID must be a string");
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
|
||||
// Error thrown by us will be dropped when entering V8.
|
||||
// Make sure to abort early and propagate the error to JS.
|
||||
|
@ -392,6 +392,15 @@ gfx::Rect Tray::GetBounds() {
|
|||
return tray_icon_->GetBounds();
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> Tray::GetGUID() {
|
||||
if (!CheckAlive())
|
||||
return {};
|
||||
auto* isolate = JavascriptEnvironment::GetIsolate();
|
||||
if (!guid_)
|
||||
return v8::Null(isolate);
|
||||
return gin::ConvertToV8(isolate, guid_.value());
|
||||
}
|
||||
|
||||
bool Tray::CheckAlive() {
|
||||
if (!tray_icon_) {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
|
@ -424,6 +433,7 @@ void Tray::FillObjectTemplate(v8::Isolate* isolate,
|
|||
.SetMethod("closeContextMenu", &Tray::CloseContextMenu)
|
||||
.SetMethod("setContextMenu", &Tray::SetContextMenu)
|
||||
.SetMethod("getBounds", &Tray::GetBounds)
|
||||
.SetMethod("getGUID", &Tray::GetGUID)
|
||||
.Build();
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
|
|||
// gin_helper::Constructible
|
||||
static gin_helper::Handle<Tray> New(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<UUID> guid,
|
||||
std::optional<base::Uuid> guid,
|
||||
gin::Arguments* args);
|
||||
|
||||
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
|
||||
|
@ -65,7 +65,7 @@ class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
|
|||
private:
|
||||
Tray(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<UUID> guid);
|
||||
std::optional<base::Uuid> guid);
|
||||
~Tray() override;
|
||||
|
||||
// TrayIconObserver:
|
||||
|
@ -111,10 +111,12 @@ class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
|
|||
void SetContextMenu(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> arg);
|
||||
gfx::Rect GetBounds();
|
||||
v8::Local<v8::Value> GetGUID();
|
||||
|
||||
bool CheckAlive();
|
||||
|
||||
v8::Global<v8::Value> menu_;
|
||||
std::optional<base::Uuid> guid_;
|
||||
std::unique_ptr<TrayIcon> tray_icon_;
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ gfx::Rect TrayIcon::GetBounds() {
|
|||
return {};
|
||||
}
|
||||
|
||||
void TrayIcon::SetAutoSaveName(const std::string& name) {}
|
||||
|
||||
void TrayIcon::NotifyClicked(const gfx::Rect& bounds,
|
||||
const gfx::Point& location,
|
||||
int modifiers) {
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace electron {
|
|||
|
||||
class TrayIcon {
|
||||
public:
|
||||
static TrayIcon* Create(std::optional<UUID> guid);
|
||||
static TrayIcon* Create(std::optional<base::Uuid> guid);
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
using ImageType = HICON;
|
||||
|
@ -99,6 +99,8 @@ class TrayIcon {
|
|||
// Returns the bounds of tray icon.
|
||||
virtual gfx::Rect GetBounds();
|
||||
|
||||
virtual void SetAutoSaveName(const std::string& name);
|
||||
|
||||
void AddObserver(TrayIconObserver* obs) { observers_.AddObserver(obs); }
|
||||
void RemoveObserver(TrayIconObserver* obs) { observers_.RemoveObserver(obs); }
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ class TrayIconCocoa : public TrayIcon {
|
|||
void CloseContextMenu() override;
|
||||
void SetContextMenu(raw_ptr<ElectronMenuModel> menu_model) override;
|
||||
gfx::Rect GetBounds() override;
|
||||
void SetAutoSaveName(const std::string& name) override;
|
||||
|
||||
base::WeakPtr<TrayIconCocoa> GetWeakPtr() {
|
||||
return weak_factory_.GetWeakPtr();
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "base/message_loop/message_pump_apple.h"
|
||||
#include "base/strings/sys_string_conversions.h"
|
||||
#include "base/task/current_thread.h"
|
||||
#include "base/uuid.h"
|
||||
#include "content/public/browser/browser_task_traits.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "shell/browser/ui/cocoa/NSString+ANSI.h"
|
||||
|
@ -68,6 +69,10 @@
|
|||
[self setFrame:[statusItem_ button].frame];
|
||||
}
|
||||
|
||||
- (void)setAutosaveName:(NSString*)name {
|
||||
statusItem_.autosaveName = name;
|
||||
}
|
||||
|
||||
- (void)updateTrackingAreas {
|
||||
// Use NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
|
||||
// events.
|
||||
|
@ -420,8 +425,12 @@ gfx::Rect TrayIconCocoa::GetBounds() {
|
|||
return gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
|
||||
}
|
||||
|
||||
void TrayIconCocoa::SetAutoSaveName(const std::string& name) {
|
||||
[status_item_view_ setAutosaveName:base::SysUTF8ToNSString(name)];
|
||||
}
|
||||
|
||||
// static
|
||||
TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
|
||||
TrayIcon* TrayIcon::Create(std::optional<base::Uuid> guid) {
|
||||
return new TrayIconCocoa;
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ ui::StatusIconLinux* TrayIconLinux::GetStatusIcon() {
|
|||
}
|
||||
|
||||
// static
|
||||
TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
|
||||
TrayIcon* TrayIcon::Create(std::optional<base::Uuid> guid) {
|
||||
return new TrayIconLinux;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
namespace electron {
|
||||
|
||||
// static
|
||||
TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
|
||||
TrayIcon* TrayIcon::Create(std::optional<base::Uuid> guid) {
|
||||
static NotifyIconHost host;
|
||||
return host.CreateNotifyIcon(guid);
|
||||
}
|
||||
|
|
|
@ -190,21 +190,32 @@ NotifyIconHost::~NotifyIconHost() {
|
|||
delete ptr;
|
||||
}
|
||||
|
||||
NotifyIcon* NotifyIconHost::CreateNotifyIcon(std::optional<UUID> guid) {
|
||||
if (guid.has_value()) {
|
||||
for (NotifyIcons::const_iterator i(notify_icons_.begin());
|
||||
i != notify_icons_.end(); ++i) {
|
||||
auto* 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* NotifyIconHost::CreateNotifyIcon(std::optional<base::Uuid> guid) {
|
||||
std::string guid_str =
|
||||
guid.has_value() ? guid.value().AsLowercaseString() : "";
|
||||
UUID uid = GUID_NULL;
|
||||
if (!guid_str.empty()) {
|
||||
if (guid_str[0] == '{' && guid_str[guid_str.length() - 1] == '}') {
|
||||
guid_str = guid_str.substr(1, guid_str.length() - 2);
|
||||
}
|
||||
|
||||
unsigned char* uid_cstr = (unsigned char*)guid_str.c_str();
|
||||
RPC_STATUS result = UuidFromStringA(uid_cstr, &uid);
|
||||
if (result != RPC_S_INVALID_STRING_UUID) {
|
||||
for (NotifyIcons::const_iterator i(notify_icons_.begin());
|
||||
i != notify_icons_.end(); ++i) {
|
||||
auto* current_win_icon = static_cast<NotifyIcon*>(*i);
|
||||
if (current_win_icon->guid() == uid) {
|
||||
LOG(WARNING)
|
||||
<< "Guid already in use. Existing tray entry will be replaced.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto* notify_icon =
|
||||
new NotifyIcon(this, NextIconId(), window_, kNotifyIconMessage,
|
||||
guid.has_value() ? guid.value() : GUID_DEFAULT);
|
||||
uid == GUID_NULL ? GUID_DEFAULT : uid);
|
||||
|
||||
notify_icons_.push_back(notify_icon);
|
||||
return notify_icon;
|
||||
|
|
|
@ -27,7 +27,7 @@ class NotifyIconHost {
|
|||
NotifyIconHost(const NotifyIconHost&) = delete;
|
||||
NotifyIconHost& operator=(const NotifyIconHost&) = delete;
|
||||
|
||||
NotifyIcon* CreateNotifyIcon(std::optional<UUID> guid);
|
||||
NotifyIcon* CreateNotifyIcon(std::optional<base::Uuid> guid);
|
||||
void Remove(NotifyIcon* notify_icon);
|
||||
|
||||
private:
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
#include <string>
|
||||
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/uuid.h"
|
||||
#include "gin/converter.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
|
@ -36,18 +38,40 @@ typedef struct {
|
|||
|
||||
namespace gin {
|
||||
|
||||
template <>
|
||||
struct Converter<base::Uuid> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
base::Uuid* out) {
|
||||
std::string guid;
|
||||
if (!gin::ConvertFromV8(isolate, val, &guid))
|
||||
return false;
|
||||
|
||||
base::Uuid parsed = base::Uuid::ParseLowercase(base::ToLowerASCII(guid));
|
||||
if (!parsed.is_valid())
|
||||
return false;
|
||||
|
||||
*out = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate, base::Uuid val) {
|
||||
const std::string guid = val.AsLowercaseString();
|
||||
return gin::ConvertToV8(isolate, guid);
|
||||
}
|
||||
};
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
template <>
|
||||
struct Converter<UUID> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
UUID* out) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
std::string guid;
|
||||
if (!gin::ConvertFromV8(isolate, val, &guid))
|
||||
return false;
|
||||
|
||||
UUID uid;
|
||||
|
||||
if (!guid.empty()) {
|
||||
if (guid[0] == '{' && guid[guid.length() - 1] == '}') {
|
||||
guid = guid.substr(1, guid.length() - 2);
|
||||
|
@ -62,12 +86,8 @@ struct Converter<UUID> {
|
|||
}
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate, UUID val) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
const GUID GUID_NULL = {};
|
||||
if (val == GUID_NULL) {
|
||||
return v8::String::Empty(isolate);
|
||||
|
@ -75,11 +95,9 @@ struct Converter<UUID> {
|
|||
std::wstring uid = base::win::WStringFromGUID(val);
|
||||
return StringToV8(isolate, base::SysWideToUTF8(uid));
|
||||
}
|
||||
#else
|
||||
return v8::Undefined(isolate);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace gin
|
||||
|
||||
|
|
|
@ -30,13 +30,13 @@ describe('tray module', () => {
|
|||
}).to.throw(/Failed to load image from path (.+)/);
|
||||
});
|
||||
|
||||
ifit(process.platform === 'win32')('throws a descriptive error if an invalid guid is given', () => {
|
||||
ifit(process.platform !== 'linux')('throws a descriptive error if an invalid 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', () => {
|
||||
ifit(process.platform !== 'linux')('accepts a valid guid', () => {
|
||||
expect(() => {
|
||||
tray = new Tray(nativeImage.createEmpty(), '0019A433-3526-48BA-A66C-676742C0FEFB');
|
||||
}).to.not.throw();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue