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:
Shelley Vohr
2025-08-07 19:25:50 +02:00
committed by GitHub
parent f49a645c06
commit a0d983e4b5
13 changed files with 101 additions and 34 deletions

View File

@@ -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();
}

View File

@@ -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_;
};

View File

@@ -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) {

View File

@@ -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); }

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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