@yufengtadian/freedom-cli 1.12.18 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build.js +1 -1
- package/lib/security.js +1 -1
- package/package.json +5 -1
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/go.mod +1 -7
- package/templates/go/go.sum +2 -2
- package/templates/go/main.go +22 -8
- package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
- package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
- package/templates/go/pkg/freedom/assets/freedom.js +200 -5
- package/templates/go/pkg/freedom/assets_embed.go +11 -13
- package/templates/go/pkg/freedom/authenticode_other.go +11 -0
- package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
- package/templates/go/pkg/freedom/backend_proc.go +291 -53
- package/templates/go/pkg/freedom/bridge.go +13 -2
- package/templates/go/pkg/freedom/capability.go +77 -0
- package/templates/go/pkg/freedom/center_other.go +3 -0
- package/templates/go/pkg/freedom/center_windows.go +45 -35
- package/templates/go/pkg/freedom/dispatch.go +60 -0
- package/templates/go/pkg/freedom/events_windows.go +319 -0
- package/templates/go/pkg/freedom/freedom.go +193 -99
- package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
- package/templates/go/pkg/freedom/osver_other.go +15 -0
- package/templates/go/pkg/freedom/osver_windows.go +121 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
- package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
- package/templates/go/pkg/freedom/security.go +19 -12
- package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
- package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
- package/templates/go/pkg/freedom/spawn_other.go +11 -0
- package/templates/go/pkg/freedom/spawn_windows.go +23 -0
- package/templates/go/pkg/freedom/store.go +458 -0
- package/templates/go/pkg/freedom/syscap_linux.go +278 -0
- package/templates/go/pkg/freedom/syscap_other.go +15 -3
- package/templates/go/pkg/freedom/syscap_windows.go +228 -36
- package/templates/go/pkg/freedom/sysint_common.go +70 -0
- package/templates/go/pkg/freedom/sysint_windows.go +426 -0
- package/templates/go/pkg/freedom/tray_common.go +12 -0
- package/templates/go/pkg/freedom/tray_linux.go +302 -0
- package/templates/go/pkg/freedom/tray_other.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +69 -39
- package/templates/go/pkg/freedom/updater.go +372 -0
- package/templates/go/pkg/freedom/window_mgr.go +385 -0
- package/templates/go/pkg/freedom/window_other.go +32 -66
- package/templates/go/pkg/freedom/window_windows.go +397 -254
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +0 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/center_windows_test.go +0 -32
- package/templates/go/pkg/freedom/security_test.go +0 -153
- package/templates/go/temp_vet_test/main.go +0 -40
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +0 -1
- package/templates/go/vet_err2.txt +0 -2
- package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
- package/templates/go/webview_go/CHANGELOG.md +0 -15
- package/templates/go/webview_go/LICENSE +0 -22
- package/templates/go/webview_go/README.md +0 -50
- package/templates/go/webview_go/examples/basic/main.go +0 -12
- package/templates/go/webview_go/examples/bind/main.go +0 -38
- package/templates/go/webview_go/glue.c +0 -36
- package/templates/go/webview_go/go.mod +0 -3
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
- package/templates/go/webview_go/libs/webview/LICENSE +0 -22
- package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
- package/templates/go/webview_go/libs/webview/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/version.txt +0 -1
- package/templates/go/webview_go/webview.cc +0 -1
- package/templates/go/webview_go/webview.go +0 -481
- package/templates/go/webview_go/webview_test.go +0 -50
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
//go:build linux
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
#cgo pkg-config: gtk+-3.0
|
|
7
|
+
#cgo CFLAGS: -Wno-deprecated-declarations
|
|
8
|
+
#include <gtk/gtk.h>
|
|
9
|
+
|
|
10
|
+
extern void goTrayClick(void);
|
|
11
|
+
extern void goTrayMenuItemActivate(int idx);
|
|
12
|
+
|
|
13
|
+
// GtkStatusIcon 自 GTK3.14 起标记 deprecated,但 legacy tray 协议在 KDE/XFCE/
|
|
14
|
+
// WSLg 仍广泛可用;AppIndicator(GDBus) 适配在未来自成一档。
|
|
15
|
+
static int ensureGtk(void) { return gtk_init_check(NULL, NULL) ? 1 : 0; }
|
|
16
|
+
|
|
17
|
+
static void onTrayActivate(GtkStatusIcon *icon, gpointer data) {
|
|
18
|
+
(void)icon; (void)data;
|
|
19
|
+
goTrayClick();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 右键:弹出前端注册的菜单(popup-menu 信号),返回 TRUE 抑制默认行为。
|
|
23
|
+
static gboolean onTrayPopupMenu(GtkStatusIcon *icon, guint button, guint activate_time, gpointer data) {
|
|
24
|
+
(void)button; (void)activate_time; (void)data;
|
|
25
|
+
GtkWidget *menu = (GtkWidget*) g_object_get_data(G_OBJECT(icon), "freedom-menu");
|
|
26
|
+
if (menu != NULL) gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
|
|
27
|
+
return TRUE;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static void onMenuItemClick(GtkMenuItem *item, gpointer data) {
|
|
31
|
+
(void)item;
|
|
32
|
+
goTrayMenuItemActivate(GPOINTER_TO_INT(data));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static GtkStatusIcon* trayNewFromPng(const guint8 *png, gsize len) {
|
|
36
|
+
GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
|
|
37
|
+
gboolean ok = gdk_pixbuf_loader_write(loader, png, len, NULL);
|
|
38
|
+
ok = ok && gdk_pixbuf_loader_close(loader, NULL);
|
|
39
|
+
GdkPixbuf *pb = ok ? gdk_pixbuf_loader_get_pixbuf(loader) : NULL;
|
|
40
|
+
GtkStatusIcon *icon = pb ? gtk_status_icon_new_from_pixbuf(pb) : NULL;
|
|
41
|
+
g_object_unref(loader);
|
|
42
|
+
if (icon == NULL) return NULL;
|
|
43
|
+
g_signal_connect(icon, "activate", G_CALLBACK(onTrayActivate), NULL);
|
|
44
|
+
g_signal_connect(icon, "popup-menu", G_CALLBACK(onTrayPopupMenu), NULL);
|
|
45
|
+
gtk_status_icon_set_visible(icon, TRUE);
|
|
46
|
+
return icon;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static GtkStatusIcon* trayNewNamed(const char *name) {
|
|
50
|
+
GtkStatusIcon *icon = gtk_status_icon_new_from_icon_name(name);
|
|
51
|
+
g_signal_connect(icon, "activate", G_CALLBACK(onTrayActivate), NULL);
|
|
52
|
+
g_signal_connect(icon, "popup-menu", G_CALLBACK(onTrayPopupMenu), NULL);
|
|
53
|
+
gtk_status_icon_set_visible(icon, TRUE);
|
|
54
|
+
return icon;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static void traySetTooltip(GtkStatusIcon *icon, const char *tip) {
|
|
58
|
+
gtk_status_icon_set_tooltip_text(icon, tip);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static void trayDestroy(GtkStatusIcon *icon) {
|
|
62
|
+
gtk_status_icon_set_visible(icon, FALSE);
|
|
63
|
+
g_object_unref(icon);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 菜单所有权:attach 时 ref_sink + set_data_full,重复 attach 自动释放旧菜单。
|
|
67
|
+
static void trayAttachMenu(GtkStatusIcon *icon, GtkWidget *menu) {
|
|
68
|
+
g_object_ref_sink(menu);
|
|
69
|
+
g_object_set_data_full(G_OBJECT(icon), "freedom-menu", menu, (GDestroyNotify) g_object_unref);
|
|
70
|
+
gtk_widget_show_all(menu);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static GtkWidget* menuNew(void) { return gtk_menu_new(); }
|
|
74
|
+
|
|
75
|
+
static GtkWidget* menuSubmenu(GtkWidget *parent, const char *label) {
|
|
76
|
+
GtkWidget *item = gtk_menu_item_new_with_label(label);
|
|
77
|
+
GtkWidget *sub = gtk_menu_new();
|
|
78
|
+
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
|
|
79
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(parent), item);
|
|
80
|
+
return sub;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static void menuAppendItem(GtkWidget *menu, const char *label, int idx, gboolean sensitive) {
|
|
84
|
+
GtkWidget *item = gtk_menu_item_new_with_label(label);
|
|
85
|
+
gtk_widget_set_sensitive(item, sensitive);
|
|
86
|
+
g_signal_connect(item, "activate", G_CALLBACK(onMenuItemClick), GINT_TO_POINTER(idx));
|
|
87
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static void menuAppendCheck(GtkWidget *menu, const char *label, int idx, gboolean checked, gboolean sensitive) {
|
|
91
|
+
GtkWidget *item = gtk_check_menu_item_new_with_label(label);
|
|
92
|
+
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), checked);
|
|
93
|
+
gtk_widget_set_sensitive(item, sensitive);
|
|
94
|
+
g_signal_connect(item, "activate", G_CALLBACK(onMenuItemClick), GINT_TO_POINTER(idx));
|
|
95
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static void menuAppendSeparator(GtkWidget *menu) {
|
|
99
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
|
100
|
+
}
|
|
101
|
+
*/
|
|
102
|
+
import "C"
|
|
103
|
+
|
|
104
|
+
import (
|
|
105
|
+
"encoding/json"
|
|
106
|
+
"fmt"
|
|
107
|
+
"sync"
|
|
108
|
+
"unsafe"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// M4 Linux 托盘(GtkStatusIcon,cgo):与 Windows 侧 trayCall 同一前端契约
|
|
112
|
+
// (tray.create/destroy/tooltip/menu;事件 tray:click / tray:menu({id}))。
|
|
113
|
+
// 限制:GtkStatusIcon 无双击信号(tray:double-click 不触发);menu.set(主窗口
|
|
114
|
+
// 原生菜单栏)与 GDBus StatusNotifierItem 适配未实装,托盘图标走 legacy
|
|
115
|
+
// system tray 协议(KDE/xfce/wmctrl 系支持;GNOME 需 AppIndicator 扩展)。
|
|
116
|
+
// GTK 调用须发生在 main线程:__freedom_tray 桥回调经 WebKit 消息处理器在
|
|
117
|
+
// GTK main loop 线程执行,天然满足;单测须先 gtk_init_check 且有 DISPLAY。
|
|
118
|
+
|
|
119
|
+
type linuxTrayState struct {
|
|
120
|
+
mu sync.Mutex
|
|
121
|
+
icon *C.GtkStatusIcon
|
|
122
|
+
items map[int32]string // 菜单条目序号 → 前端 id
|
|
123
|
+
emit func(event string, data interface{})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var linuxTray = &linuxTrayState{items: map[int32]string{}}
|
|
127
|
+
|
|
128
|
+
//export goTrayClick
|
|
129
|
+
func goTrayClick() {
|
|
130
|
+
linuxTray.mu.Lock()
|
|
131
|
+
emit := linuxTray.emit
|
|
132
|
+
linuxTray.mu.Unlock()
|
|
133
|
+
if emit != nil {
|
|
134
|
+
emit("tray:click", map[string]interface{}{"button": "left"})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//export goTrayMenuItemActivate
|
|
139
|
+
func goTrayMenuItemActivate(idx C.int) {
|
|
140
|
+
linuxTray.mu.Lock()
|
|
141
|
+
id := linuxTray.items[int32(idx)]
|
|
142
|
+
emit := linuxTray.emit
|
|
143
|
+
linuxTray.mu.Unlock()
|
|
144
|
+
if emit != nil {
|
|
145
|
+
emit("tray:menu", map[string]interface{}{"id": id})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// trayCall 处理前端 __freedom_tray 请求(Linux 实现,方法面与 Windows 对齐)。
|
|
150
|
+
func (a *App) trayCall(method string, paramsJSON string) (result interface{}, err error) {
|
|
151
|
+
defer func() {
|
|
152
|
+
if r := recover(); r != nil {
|
|
153
|
+
result = nil
|
|
154
|
+
err = fmt.Errorf("freedom: tray method %q panicked: %v", method, r)
|
|
155
|
+
}
|
|
156
|
+
}()
|
|
157
|
+
|
|
158
|
+
var args map[string]json.RawMessage
|
|
159
|
+
if len(paramsJSON) > 0 && paramsJSON != "null" {
|
|
160
|
+
if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
|
|
161
|
+
return nil, fmt.Errorf("freedom: tray method %q: invalid args: %w", method, err)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
argStr := func(k string) string {
|
|
165
|
+
if v, ok := args[k]; ok {
|
|
166
|
+
var s string
|
|
167
|
+
if json.Unmarshal(v, &s) == nil {
|
|
168
|
+
return s
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return ""
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
switch method {
|
|
175
|
+
case "tray.create":
|
|
176
|
+
return nil, linuxTrayCreate(a, argStr("icon"), argStr("tooltip"))
|
|
177
|
+
case "tray.destroy":
|
|
178
|
+
linuxTrayDestroy()
|
|
179
|
+
return nil, nil
|
|
180
|
+
case "tray.tooltip":
|
|
181
|
+
linuxTray.mu.Lock()
|
|
182
|
+
icon := linuxTray.icon
|
|
183
|
+
linuxTray.mu.Unlock()
|
|
184
|
+
if icon == nil {
|
|
185
|
+
return nil, fmt.Errorf("tray: 托盘未创建")
|
|
186
|
+
}
|
|
187
|
+
tip := C.CString(argStr("tooltip"))
|
|
188
|
+
C.traySetTooltip(icon, tip)
|
|
189
|
+
C.free(unsafe.Pointer(tip))
|
|
190
|
+
return nil, nil
|
|
191
|
+
case "tray.menu":
|
|
192
|
+
linuxTray.mu.Lock()
|
|
193
|
+
icon := linuxTray.icon
|
|
194
|
+
linuxTray.mu.Unlock()
|
|
195
|
+
if icon == nil {
|
|
196
|
+
return nil, fmt.Errorf("tray: 托盘未创建")
|
|
197
|
+
}
|
|
198
|
+
var items []menuItem
|
|
199
|
+
if v, ok := args["items"]; ok {
|
|
200
|
+
if err := json.Unmarshal(v, &items); err != nil {
|
|
201
|
+
return nil, fmt.Errorf("tray: 菜单格式错误: %w", err)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
linuxTraySetMenu(icon, items)
|
|
205
|
+
return nil, nil
|
|
206
|
+
case "menu.set":
|
|
207
|
+
return nil, fmt.Errorf("freedom: menu.set 在 linux 暂不支持(GTK 菜单栏待补)")
|
|
208
|
+
default:
|
|
209
|
+
return nil, fmt.Errorf("freedom: unknown tray method %q", method)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func linuxTrayCreate(a *App, iconDataURL, tooltip string) error {
|
|
214
|
+
if C.ensureGtk() == 0 {
|
|
215
|
+
return fmt.Errorf("freedom: 无可用显示服务(DISPLAY/Wayland),无法创建托盘")
|
|
216
|
+
}
|
|
217
|
+
png := dataURLToBytes(iconDataURL)
|
|
218
|
+
var icon *C.GtkStatusIcon
|
|
219
|
+
if len(png) > 0 {
|
|
220
|
+
icon = C.trayNewFromPng((*C.guint8)(unsafe.Pointer(&png[0])), C.gsize(len(png)))
|
|
221
|
+
} else {
|
|
222
|
+
name := C.CString("application-x-executable")
|
|
223
|
+
icon = C.trayNewNamed(name)
|
|
224
|
+
C.free(unsafe.Pointer(name))
|
|
225
|
+
}
|
|
226
|
+
if icon == nil {
|
|
227
|
+
return fmt.Errorf("freedom: 托盘图标创建失败(PNG 无法解析或无托盘协议支持)")
|
|
228
|
+
}
|
|
229
|
+
if tooltip != "" {
|
|
230
|
+
tip := C.CString(tooltip)
|
|
231
|
+
C.traySetTooltip(icon, tip)
|
|
232
|
+
C.free(unsafe.Pointer(tip))
|
|
233
|
+
}
|
|
234
|
+
linuxTray.mu.Lock()
|
|
235
|
+
old := linuxTray.icon
|
|
236
|
+
linuxTray.icon = icon
|
|
237
|
+
linuxTray.emit = a.Emit
|
|
238
|
+
linuxTray.mu.Unlock()
|
|
239
|
+
if old != nil {
|
|
240
|
+
C.trayDestroy(old)
|
|
241
|
+
}
|
|
242
|
+
return nil
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func linuxTrayDestroy() {
|
|
246
|
+
linuxTray.mu.Lock()
|
|
247
|
+
icon := linuxTray.icon
|
|
248
|
+
linuxTray.icon = nil
|
|
249
|
+
linuxTray.items = map[int32]string{}
|
|
250
|
+
linuxTray.mu.Unlock()
|
|
251
|
+
if icon != nil {
|
|
252
|
+
C.trayDestroy(icon)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// linuxTraySetMenu 重建右键菜单并接管 icon 上的旧菜单释放(set_data_full)。
|
|
257
|
+
func linuxTraySetMenu(icon *C.GtkStatusIcon, items []menuItem) {
|
|
258
|
+
linuxTray.mu.Lock()
|
|
259
|
+
linuxTray.items = map[int32]string{}
|
|
260
|
+
var next int32
|
|
261
|
+
assign := func(id string) int32 {
|
|
262
|
+
h := next
|
|
263
|
+
next++
|
|
264
|
+
linuxTray.items[h] = id
|
|
265
|
+
return h
|
|
266
|
+
}
|
|
267
|
+
var build func(menu *C.GtkWidget, list []menuItem)
|
|
268
|
+
build = func(menu *C.GtkWidget, list []menuItem) {
|
|
269
|
+
for _, it := range list {
|
|
270
|
+
label := C.CString(it.Label)
|
|
271
|
+
switch {
|
|
272
|
+
case it.Type == "separator":
|
|
273
|
+
C.menuAppendSeparator(menu)
|
|
274
|
+
case it.Type == "submenu" || len(it.Submenu) > 0:
|
|
275
|
+
sub := C.menuSubmenu(menu, label)
|
|
276
|
+
// 父条目无动作,仅展开;子条目递归编号。
|
|
277
|
+
assign(it.ID)
|
|
278
|
+
build(sub, it.Submenu)
|
|
279
|
+
default:
|
|
280
|
+
idx := assign(it.ID)
|
|
281
|
+
sensitive := C.gboolean(1)
|
|
282
|
+
if it.Enabled != nil && !*it.Enabled {
|
|
283
|
+
sensitive = 0
|
|
284
|
+
}
|
|
285
|
+
if it.Checked != nil {
|
|
286
|
+
checked := C.gboolean(0)
|
|
287
|
+
if *it.Checked {
|
|
288
|
+
checked = 1
|
|
289
|
+
}
|
|
290
|
+
C.menuAppendCheck(menu, label, C.int(idx), checked, sensitive)
|
|
291
|
+
} else {
|
|
292
|
+
C.menuAppendItem(menu, label, C.int(idx), sensitive)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
C.free(unsafe.Pointer(label))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
menu := C.menuNew()
|
|
299
|
+
build(menu, items)
|
|
300
|
+
C.trayAttachMenu(icon, menu)
|
|
301
|
+
linuxTray.mu.Unlock()
|
|
302
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//go:build !windows
|
|
1
|
+
//go:build !windows && !linux
|
|
2
2
|
|
|
3
3
|
package freedom
|
|
4
4
|
|
|
5
5
|
import "fmt"
|
|
6
6
|
|
|
7
|
-
// trayCall 非 Windows 平台暂不提供系统托盘与原生菜单栏。
|
|
7
|
+
// trayCall 非 Windows/Linux 平台暂不提供系统托盘与原生菜单栏。Linux 实装见 tray_linux.go(M4)。
|
|
8
8
|
func (a *App) trayCall(method string, paramsJSON string) (interface{}, error) {
|
|
9
9
|
return nil, fmt.Errorf("freedom: tray method %q is not supported on this platform", method)
|
|
10
10
|
}
|
|
@@ -108,26 +108,18 @@ type notifyIconDataW struct {
|
|
|
108
108
|
HBalloonIcon uintptr
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// menuItem 描述一个托盘/菜单栏条目(JSON 透传)。
|
|
112
|
-
type menuItem struct {
|
|
113
|
-
ID string `json:"id"`
|
|
114
|
-
Label string `json:"label"`
|
|
115
|
-
Type string `json:"type"` // "item"(默认)/ "separator" / "submenu"
|
|
116
|
-
Enabled *bool `json:"enabled"` // nil=默认启用
|
|
117
|
-
Checked *bool `json:"checked"`
|
|
118
|
-
Submenu []menuItem `json:"submenu"`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
111
|
// trayState 保存当前托盘实例(单窗口框架内为单例)。
|
|
122
112
|
type trayState struct {
|
|
123
|
-
mu
|
|
124
|
-
hwnd
|
|
125
|
-
menu
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
mu sync.Mutex
|
|
114
|
+
hwnd uintptr // 隐藏消息窗口
|
|
115
|
+
menu uintptr // 右键 popup 菜单
|
|
116
|
+
menuIDs []uint32 // menu 占用的条目 id(重建时清理映射)
|
|
117
|
+
menuBar uintptr // 原生菜单栏 HMENU
|
|
118
|
+
menuBarIDs []uint32 // menuBar 占用的条目 id
|
|
119
|
+
items map[uint32]string // menu id -> 前端 id
|
|
120
|
+
nextID uint32
|
|
121
|
+
emit func(event string, data interface{})
|
|
122
|
+
icon uintptr // 当前托盘图标 HICON
|
|
131
123
|
}
|
|
132
124
|
|
|
133
125
|
var (
|
|
@@ -282,32 +274,41 @@ func trayShowMenu(ts *trayState) {
|
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
// buildMenu 递归构建 HMENU(用于托盘右键与菜单栏子菜单)。
|
|
285
|
-
//
|
|
286
|
-
func buildMenu(ts *trayState, items []menuItem) uintptr {
|
|
277
|
+
// 返回菜单句柄与本次分配的全部条目 id(供整体替换时清理旧映射)。
|
|
278
|
+
func buildMenu(ts *trayState, items []menuItem) (uintptr, []uint32) {
|
|
287
279
|
hMenu, _, _ := procCreatePopupMenu.Call()
|
|
288
280
|
if hMenu == 0 {
|
|
289
|
-
return 0
|
|
281
|
+
return 0, nil
|
|
290
282
|
}
|
|
291
|
-
|
|
292
|
-
|
|
283
|
+
var ids []uint32
|
|
284
|
+
appendMenuItems(ts, hMenu, items, false, &ids)
|
|
285
|
+
return hMenu, ids
|
|
293
286
|
}
|
|
294
287
|
|
|
295
|
-
// appendMenuItems
|
|
296
|
-
func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool) {
|
|
297
|
-
for
|
|
288
|
+
// appendMenuItems 把条目追加到指定菜单;新分配的 id 记入 *ids。
|
|
289
|
+
func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool, ids *[]uint32) {
|
|
290
|
+
for _, it := range items {
|
|
298
291
|
if it.Type == "separator" {
|
|
299
292
|
procAppendMenuW.Call(hMenu, mfSeparator, 0, 0)
|
|
300
293
|
continue
|
|
301
294
|
}
|
|
302
295
|
if it.Type == "submenu" || (isMenuBar && len(it.Submenu) > 0) {
|
|
303
|
-
|
|
296
|
+
label, err := syscall.UTF16PtrFromString(it.Label)
|
|
297
|
+
if err != nil {
|
|
298
|
+
continue // 标签含 NUL:跳过该条目
|
|
299
|
+
}
|
|
300
|
+
sub, subIDs := buildMenu(ts, it.Submenu)
|
|
304
301
|
if sub == 0 {
|
|
305
302
|
continue
|
|
306
303
|
}
|
|
307
|
-
|
|
304
|
+
*ids = append(*ids, subIDs...)
|
|
308
305
|
procAppendMenuW.Call(hMenu, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
|
|
309
306
|
continue
|
|
310
307
|
}
|
|
308
|
+
label, err := syscall.UTF16PtrFromString(it.Label)
|
|
309
|
+
if err != nil {
|
|
310
|
+
continue // 标签含 NUL:跳过该条目(id 不消耗)
|
|
311
|
+
}
|
|
311
312
|
var flags uintptr = mfString | mfEnabled
|
|
312
313
|
if it.Checked != nil && *it.Checked {
|
|
313
314
|
flags |= mfChecked
|
|
@@ -315,46 +316,62 @@ func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar b
|
|
|
315
316
|
if it.Enabled != nil && !*it.Enabled {
|
|
316
317
|
flags |= mfGrayed
|
|
317
318
|
}
|
|
319
|
+
ts.mu.Lock()
|
|
318
320
|
id := ts.nextID
|
|
319
321
|
ts.nextID++
|
|
320
|
-
ts.mu.Lock()
|
|
321
322
|
ts.items[id] = it.ID
|
|
322
323
|
ts.mu.Unlock()
|
|
323
|
-
|
|
324
|
-
if isMenuBar && i == 0 {
|
|
325
|
-
// 菜单栏首个条目用 MF_BYPOSITION 占位(顶层菜单由 SetMenu 接管)。
|
|
326
|
-
}
|
|
324
|
+
*ids = append(*ids, id)
|
|
327
325
|
procAppendMenuW.Call(hMenu, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
|
|
328
326
|
}
|
|
329
327
|
}
|
|
330
328
|
|
|
331
|
-
// traySetMenu
|
|
329
|
+
// traySetMenu 设置托盘右键菜单(整体替换,旧 id 映射随之清理)。
|
|
332
330
|
func traySetMenu(ts *trayState, items []menuItem) {
|
|
333
331
|
ts.mu.Lock()
|
|
334
332
|
old := ts.menu
|
|
333
|
+
oldIDs := ts.menuIDs
|
|
335
334
|
ts.menu = 0
|
|
335
|
+
ts.menuIDs = nil
|
|
336
336
|
ts.mu.Unlock()
|
|
337
337
|
if old != 0 {
|
|
338
338
|
procDestroyMenu.Call(old)
|
|
339
339
|
}
|
|
340
|
+
releaseItemIDs(ts, oldIDs)
|
|
340
341
|
if len(items) == 0 {
|
|
341
342
|
return
|
|
342
343
|
}
|
|
343
|
-
h := buildMenu(ts, items)
|
|
344
|
+
h, ids := buildMenu(ts, items)
|
|
344
345
|
ts.mu.Lock()
|
|
345
346
|
ts.menu = h
|
|
347
|
+
ts.menuIDs = ids
|
|
346
348
|
ts.mu.Unlock()
|
|
347
349
|
}
|
|
348
350
|
|
|
349
|
-
//
|
|
351
|
+
// releaseItemIDs 从 id→前端 id 映射中移除一批条目(防重建后残留无主映射无限增长)。
|
|
352
|
+
func releaseItemIDs(ts *trayState, ids []uint32) {
|
|
353
|
+
if len(ids) == 0 {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
ts.mu.Lock()
|
|
357
|
+
for _, id := range ids {
|
|
358
|
+
delete(ts.items, id)
|
|
359
|
+
}
|
|
360
|
+
ts.mu.Unlock()
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// menuBarSet 设置主窗口原生菜单栏(整体替换,旧 id 映射随之清理)。
|
|
350
364
|
func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
|
|
351
365
|
ts.mu.Lock()
|
|
352
366
|
old := ts.menuBar
|
|
367
|
+
oldIDs := ts.menuBarIDs
|
|
353
368
|
ts.menuBar = 0
|
|
369
|
+
ts.menuBarIDs = nil
|
|
354
370
|
ts.mu.Unlock()
|
|
355
371
|
if old != 0 {
|
|
356
372
|
procDestroyMenu.Call(old)
|
|
357
373
|
}
|
|
374
|
+
releaseItemIDs(ts, oldIDs)
|
|
358
375
|
if len(items) == 0 {
|
|
359
376
|
procSetMenu.Call(hwnd, 0)
|
|
360
377
|
procDrawMenuBar.Call(hwnd)
|
|
@@ -364,13 +381,21 @@ func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
|
|
|
364
381
|
if hBar == 0 {
|
|
365
382
|
return
|
|
366
383
|
}
|
|
384
|
+
var ids []uint32
|
|
367
385
|
for _, top := range items {
|
|
368
|
-
label, _ := syscall.UTF16PtrFromString(top.Label)
|
|
369
386
|
if top.Type == "separator" {
|
|
370
387
|
continue
|
|
371
388
|
}
|
|
389
|
+
label, err := syscall.UTF16PtrFromString(top.Label)
|
|
390
|
+
if err != nil {
|
|
391
|
+
continue // 标签含 NUL:跳过该条目
|
|
392
|
+
}
|
|
372
393
|
if len(top.Submenu) > 0 {
|
|
373
|
-
sub := buildMenu(ts, top.Submenu)
|
|
394
|
+
sub, subIDs := buildMenu(ts, top.Submenu)
|
|
395
|
+
if sub == 0 {
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
ids = append(ids, subIDs...)
|
|
374
399
|
procAppendMenuW.Call(hBar, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
|
|
375
400
|
} else {
|
|
376
401
|
// 顶层无子菜单:视为可点击菜单项。
|
|
@@ -378,16 +403,18 @@ func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
|
|
|
378
403
|
if top.Checked != nil && *top.Checked {
|
|
379
404
|
flags |= mfChecked
|
|
380
405
|
}
|
|
406
|
+
ts.mu.Lock()
|
|
381
407
|
id := ts.nextID
|
|
382
408
|
ts.nextID++
|
|
383
|
-
ts.mu.Lock()
|
|
384
409
|
ts.items[id] = top.ID
|
|
385
410
|
ts.mu.Unlock()
|
|
411
|
+
ids = append(ids, id)
|
|
386
412
|
procAppendMenuW.Call(hBar, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
|
|
387
413
|
}
|
|
388
414
|
}
|
|
389
415
|
ts.mu.Lock()
|
|
390
416
|
ts.menuBar = hBar
|
|
417
|
+
ts.menuBarIDs = ids
|
|
391
418
|
ts.mu.Unlock()
|
|
392
419
|
procSetMenu.Call(hwnd, hBar)
|
|
393
420
|
procDrawMenuBar.Call(hwnd)
|
|
@@ -413,6 +440,9 @@ func trayDestroy(ts *trayState) {
|
|
|
413
440
|
procDestroyMenu.Call(ts.menuBar)
|
|
414
441
|
ts.menuBar = 0
|
|
415
442
|
}
|
|
443
|
+
ts.menuIDs = nil
|
|
444
|
+
ts.menuBarIDs = nil
|
|
445
|
+
ts.items = map[uint32]string{} // 已持锁:直接重建映射,不留无主条目
|
|
416
446
|
if ts.icon != 0 {
|
|
417
447
|
procDestroyIcon.Call(ts.icon)
|
|
418
448
|
ts.icon = 0
|