Skip to content

Commit ce91f05

Browse files
authored
feat: allow macOS tray to maintain position (#48076)
1 parent 2a10eb7 commit ce91f05

File tree

13 files changed

+101
-34
lines changed

13 files changed

+101
-34
lines changed

docs/api/tray.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,15 @@ app.whenReady().then(() => {
7676
### `new Tray(image, [guid])`
7777

7878
* `image` ([NativeImage](native-image.md) | string)
79-
* `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.
79+
* `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.
80+
81+
**Windows**
82+
83+
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.
84+
85+
**MacOS**
86+
87+
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.
8088

8189
Creates a new tray icon associated with the `image`.
8290

@@ -324,6 +332,10 @@ Returns [`Rectangle`](structures/rectangle.md)
324332

325333
The `bounds` of this tray icon as `Object`.
326334

335+
#### `tray.getGUID()` _macOS_ _Windows_
336+
337+
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.
338+
327339
#### `tray.isDestroyed()`
328340

329341
Returns `boolean` - Whether the tray icon is destroyed.

shell/browser/api/electron_api_tray.cc

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,30 @@ gin::WrapperInfo Tray::kWrapperInfo = {gin::kEmbedderNativeGin};
5252

5353
Tray::Tray(v8::Isolate* isolate,
5454
v8::Local<v8::Value> image,
55-
std::optional<UUID> guid)
56-
: tray_icon_(TrayIcon::Create(guid)) {
55+
std::optional<base::Uuid> guid)
56+
: guid_(guid), tray_icon_(TrayIcon::Create(guid)) {
5757
SetImage(isolate, image);
5858
tray_icon_->AddObserver(this);
59+
if (guid.has_value())
60+
tray_icon_->SetAutoSaveName(guid.value().AsLowercaseString());
5961
}
6062

6163
Tray::~Tray() = default;
6264

6365
// static
6466
gin::Handle<Tray> Tray::New(gin_helper::ErrorThrower thrower,
6567
v8::Local<v8::Value> image,
66-
std::optional<UUID> guid,
68+
std::optional<base::Uuid> guid,
6769
gin::Arguments* args) {
6870
if (!Browser::Get()->is_ready()) {
6971
thrower.ThrowError("Cannot create Tray before app is ready");
7072
return {};
7173
}
7274

73-
#if BUILDFLAG(IS_WIN)
7475
if (!guid.has_value() && args->Length() > 1) {
75-
thrower.ThrowError("Invalid GUID format");
76+
thrower.ThrowError("Invalid GUID format - GUID must be a string");
7677
return {};
7778
}
78-
#endif
7979

8080
// Error thrown by us will be dropped when entering V8.
8181
// Make sure to abort early and propagate the error to JS.
@@ -392,6 +392,15 @@ gfx::Rect Tray::GetBounds() {
392392
return tray_icon_->GetBounds();
393393
}
394394

395+
v8::Local<v8::Value> Tray::GetGUID() {
396+
if (!CheckAlive())
397+
return {};
398+
auto* isolate = JavascriptEnvironment::GetIsolate();
399+
if (!guid_)
400+
return v8::Null(isolate);
401+
return gin::ConvertToV8(isolate, guid_.value());
402+
}
403+
395404
bool Tray::CheckAlive() {
396405
if (!tray_icon_) {
397406
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
@@ -424,6 +433,7 @@ void Tray::FillObjectTemplate(v8::Isolate* isolate,
424433
.SetMethod("closeContextMenu", &Tray::CloseContextMenu)
425434
.SetMethod("setContextMenu", &Tray::SetContextMenu)
426435
.SetMethod("getBounds", &Tray::GetBounds)
436+
.SetMethod("getGUID", &Tray::GetGUID)
427437
.Build();
428438
}
429439

shell/browser/api/electron_api_tray.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Tray final : public gin::Wrappable<Tray>,
4848
// gin_helper::Constructible
4949
static gin::Handle<Tray> New(gin_helper::ErrorThrower thrower,
5050
v8::Local<v8::Value> image,
51-
std::optional<UUID> guid,
51+
std::optional<base::Uuid> guid,
5252
gin::Arguments* args);
5353

5454
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
@@ -68,7 +68,7 @@ class Tray final : public gin::Wrappable<Tray>,
6868
private:
6969
Tray(v8::Isolate* isolate,
7070
v8::Local<v8::Value> image,
71-
std::optional<UUID> guid);
71+
std::optional<base::Uuid> guid);
7272
~Tray() override;
7373

7474
// TrayIconObserver:
@@ -114,10 +114,12 @@ class Tray final : public gin::Wrappable<Tray>,
114114
void SetContextMenu(gin_helper::ErrorThrower thrower,
115115
v8::Local<v8::Value> arg);
116116
gfx::Rect GetBounds();
117+
v8::Local<v8::Value> GetGUID();
117118

118119
bool CheckAlive();
119120

120121
v8::Global<v8::Value> menu_;
122+
std::optional<base::Uuid> guid_;
121123
std::unique_ptr<TrayIcon> tray_icon_;
122124
};
123125

shell/browser/ui/tray_icon.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ gfx::Rect TrayIcon::GetBounds() {
1616
return {};
1717
}
1818

19+
void TrayIcon::SetAutoSaveName(const std::string& name) {}
20+
1921
void TrayIcon::NotifyClicked(const gfx::Rect& bounds,
2022
const gfx::Point& location,
2123
int modifiers) {

shell/browser/ui/tray_icon.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace electron {
1818

1919
class TrayIcon {
2020
public:
21-
static TrayIcon* Create(std::optional<UUID> guid);
21+
static TrayIcon* Create(std::optional<base::Uuid> guid);
2222

2323
#if BUILDFLAG(IS_WIN)
2424
using ImageType = HICON;
@@ -99,6 +99,8 @@ class TrayIcon {
9999
// Returns the bounds of tray icon.
100100
virtual gfx::Rect GetBounds();
101101

102+
virtual void SetAutoSaveName(const std::string& name);
103+
102104
void AddObserver(TrayIconObserver* obs) { observers_.AddObserver(obs); }
103105
void RemoveObserver(TrayIconObserver* obs) { observers_.RemoveObserver(obs); }
104106

shell/browser/ui/tray_icon_cocoa.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class TrayIconCocoa : public TrayIcon {
3535
void CloseContextMenu() override;
3636
void SetContextMenu(raw_ptr<ElectronMenuModel> menu_model) override;
3737
gfx::Rect GetBounds() override;
38+
void SetAutoSaveName(const std::string& name) override;
3839

3940
base::WeakPtr<TrayIconCocoa> GetWeakPtr() {
4041
return weak_factory_.GetWeakPtr();

shell/browser/ui/tray_icon_cocoa.mm

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "base/message_loop/message_pump_apple.h"
1212
#include "base/strings/sys_string_conversions.h"
1313
#include "base/task/current_thread.h"
14+
#include "base/uuid.h"
1415
#include "content/public/browser/browser_task_traits.h"
1516
#include "content/public/browser/browser_thread.h"
1617
#include "shell/browser/ui/cocoa/NSString+ANSI.h"
@@ -68,6 +69,10 @@ - (void)updateDimensions {
6869
[self setFrame:[statusItem_ button].frame];
6970
}
7071

72+
- (void)setAutosaveName:(NSString*)name {
73+
statusItem_.autosaveName = name;
74+
}
75+
7176
- (void)updateTrackingAreas {
7277
// Use NSTrackingArea for listening to mouseEnter, mouseExit, and mouseMove
7378
// events.
@@ -420,8 +425,12 @@ - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
420425
return gfx::ScreenRectFromNSRect([status_item_view_ window].frame);
421426
}
422427

428+
void TrayIconCocoa::SetAutoSaveName(const std::string& name) {
429+
[status_item_view_ setAutosaveName:base::SysUTF8ToNSString(name)];
430+
}
431+
423432
// static
424-
TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
433+
TrayIcon* TrayIcon::Create(std::optional<base::Uuid> guid) {
425434
return new TrayIconCocoa;
426435
}
427436

shell/browser/ui/tray_icon_linux.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ ui::StatusIconLinux* TrayIconLinux::GetStatusIcon() {
112112
}
113113

114114
// static
115-
TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
115+
TrayIcon* TrayIcon::Create(std::optional<base::Uuid> guid) {
116116
return new TrayIconLinux;
117117
}
118118

shell/browser/ui/tray_icon_win.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
namespace electron {
99

1010
// static
11-
TrayIcon* TrayIcon::Create(std::optional<UUID> guid) {
11+
TrayIcon* TrayIcon::Create(std::optional<base::Uuid> guid) {
1212
static NotifyIconHost host;
1313
return host.CreateNotifyIcon(guid);
1414
}

shell/browser/ui/win/notify_icon_host.cc

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,21 +190,32 @@ NotifyIconHost::~NotifyIconHost() {
190190
delete ptr;
191191
}
192192

193-
NotifyIcon* NotifyIconHost::CreateNotifyIcon(std::optional<UUID> guid) {
194-
if (guid.has_value()) {
195-
for (NotifyIcons::const_iterator i(notify_icons_.begin());
196-
i != notify_icons_.end(); ++i) {
197-
auto* current_win_icon = static_cast<NotifyIcon*>(*i);
198-
if (current_win_icon->guid() == guid.value()) {
199-
LOG(WARNING)
200-
<< "Guid already in use. Existing tray entry will be replaced.";
193+
NotifyIcon* NotifyIconHost::CreateNotifyIcon(std::optional<base::Uuid> guid) {
194+
std::string guid_str =
195+
guid.has_value() ? guid.value().AsLowercaseString() : "";
196+
UUID uid = GUID_NULL;
197+
if (!guid_str.empty()) {
198+
if (guid_str[0] == '{' && guid_str[guid_str.length() - 1] == '}') {
199+
guid_str = guid_str.substr(1, guid_str.length() - 2);
200+
}
201+
202+
unsigned char* uid_cstr = (unsigned char*)guid_str.c_str();
203+
RPC_STATUS result = UuidFromStringA(uid_cstr, &uid);
204+
if (result != RPC_S_INVALID_STRING_UUID) {
205+
for (NotifyIcons::const_iterator i(notify_icons_.begin());
206+
i != notify_icons_.end(); ++i) {
207+
auto* current_win_icon = static_cast<NotifyIcon*>(*i);
208+
if (current_win_icon->guid() == uid) {
209+
LOG(WARNING)
210+
<< "Guid already in use. Existing tray entry will be replaced.";
211+
}
201212
}
202213
}
203214
}
204215

205216
auto* notify_icon =
206217
new NotifyIcon(this, NextIconId(), window_, kNotifyIconMessage,
207-
guid.has_value() ? guid.value() : GUID_DEFAULT);
218+
uid == GUID_NULL ? GUID_DEFAULT : uid);
208219

209220
notify_icons_.push_back(notify_icon);
210221
return notify_icon;

0 commit comments

Comments
 (0)