返回 DeepSeek-Reasonix
systray_menu_unix.go
根目录 / desktop / third_party / systray / systray_menu_unix.go
1 //go:build (linux || freebsd || openbsd || netbsd) && !android
2
3 package systray
4
5 import (
6 "fmt"
7 "log"
8 "os"
9 "strings"
10 "time"
11
12 "github.com/godbus/dbus/v5"
13 "github.com/godbus/dbus/v5/prop"
14
15 "fyne.io/systray/internal/generated/menu"
16 )
17
18 // SetIcon sets the icon of a menu item.
19 // iconBytes should be the content of .ico/.jpg/.png
20 func (item *MenuItem) SetIcon(iconBytes []byte) {
21 instance.menuLock.Lock()
22 defer instance.menuLock.Unlock()
23 m, exists := findLayout(int32(item.id))
24 if exists {
25 m.V1["icon-data"] = dbus.MakeVariant(iconBytes)
26 emitItemPropertiesUpdated(int32(item.id), m.V1)
27 // Property-only change; per spec LayoutUpdated isn't required here,
28 // but kept as a fallback for clients that only watch LayoutUpdated.
29 refresh()
30 }
31 }
32
33 // SetIconFromFilePath sets the icon of a menu item from a file path.
34 // iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms.
35 func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error {
36 iconBytes, err := os.ReadFile(iconFilePath)
37 if err != nil {
38 return fmt.Errorf("failed to read icon file: %v", err)
39 }
40 item.SetIcon(iconBytes)
41 return nil
42 }
43
44 // copyLayout makes full copy of layout
45 func copyLayout(in *menuLayout, depth int32) *menuLayout {
46 out := menuLayout{
47 V0: in.V0,
48 V1: make(map[string]dbus.Variant, len(in.V1)),
49 }
50 for k, v := range in.V1 {
51 out.V1[k] = v
52 }
53 if depth != 0 {
54 depth--
55 out.V2 = make([]dbus.Variant, len(in.V2))
56 for i, v := range in.V2 {
57 out.V2[i] = dbus.MakeVariant(copyLayout(v.Value().(*menuLayout), depth))
58 }
59 } else {
60 out.V2 = []dbus.Variant{}
61 }
62 return &out
63 }
64
65 // firstGetLayoutDone tracks whether the initial GetLayout response has been served
66 // since the last menu reset. libdbusmenu-gtk3 (used by Cinnamon/Xfce/GNOME) requests
67 // GetGroupProperties for grandchildren before parents in the first call, causing children
68 // to be silently dropped (blank submenus) because parent GtkMenu containers don't exist
69 // yet. Returning depth=1 on the first call ensures parents get their GtkMenu containers
70 // before grandchildren are introduced in subsequent calls, where the order is correct.
71 // After serving depth=1, a goroutine automatically triggers a second GetLayout cycle so
72 // submenus populate without requiring user interaction. Multiple goroutines from rapid
73 // resets are harmless — they just emit extra LayoutUpdated signals.
74 var firstGetLayoutDone bool
75
76 // GetLayout is com.canonical.dbusmenu.GetLayout method.
77 func (t *tray) GetLayout(parentID int32, recursionDepth int32, propertyNames []string) (revision uint32, layout menuLayout, err *dbus.Error) {
78 initialMenuBuilt.Wait()
79 instance.menuLock.Lock()
80 defer instance.menuLock.Unlock()
81 if m, ok := findLayout(parentID); ok {
82 depth := recursionDepth
83 if !firstGetLayoutDone {
84 firstGetLayoutDone = true
85 depth = 1
86 go func() {
87 time.Sleep(150 * time.Millisecond)
88 refresh()
89 }()
90 }
91 // return copy of menu layout to prevent panic from cuncurrent access to layout
92 return instance.menuVersion, *copyLayout(m, depth), nil
93 }
94 return
95 }
96
97 // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method.
98 func (t *tray) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct {
99 V0 int32
100 V1 map[string]dbus.Variant
101 }, err *dbus.Error) {
102 instance.menuLock.Lock()
103 defer instance.menuLock.Unlock()
104 for _, id := range ids {
105 if m, ok := findLayout(id); ok {
106 p := struct {
107 V0 int32
108 V1 map[string]dbus.Variant
109 }{
110 V0: m.V0,
111 V1: make(map[string]dbus.Variant, len(m.V1)),
112 }
113 for k, v := range m.V1 {
114 p.V1[k] = v
115 }
116 properties = append(properties, p)
117 }
118 }
119 return
120 }
121
122 // GetProperty is com.canonical.dbusmenu.GetProperty method.
123 func (t *tray) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) {
124 instance.menuLock.Lock()
125 defer instance.menuLock.Unlock()
126 if m, ok := findLayout(id); ok {
127 if p, ok := m.V1[name]; ok {
128 return p, nil
129 }
130 }
131 return
132 }
133
134 // Event is com.canonical.dbusmenu.Event method.
135 func (t *tray) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) (err *dbus.Error) {
136 switch eventID {
137 case "clicked":
138 systrayMenuItemSelected(uint32(id))
139 case "opened":
140 t.menuLock.RLock()
141 rootMenuID := t.menu.V0
142 t.menuLock.RUnlock()
143
144 if id == rootMenuID {
145 select {
146 case TrayOpenedCh <- struct{}{}:
147 default:
148 }
149 }
150 }
151 return
152 }
153
154 // EventGroup is com.canonical.dbusmenu.EventGroup method.
155 func (t *tray) EventGroup(events []struct {
156 V0 int32
157 V1 string
158 V2 dbus.Variant
159 V3 uint32
160 }) (idErrors []int32, err *dbus.Error) {
161 for _, event := range events {
162 if event.V1 == "clicked" {
163 systrayMenuItemSelected(uint32(event.V0))
164 }
165 }
166 return
167 }
168
169 // AboutToShow is com.canonical.dbusmenu.AboutToShow method.
170 func (t *tray) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) {
171 return
172 }
173
174 // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method.
175 func (t *tray) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) {
176 instance.menuLock.RLock()
177 defer instance.menuLock.RUnlock()
178 for _, id := range ids {
179 if m, ok := findLayout(id); ok && len(m.V2) > 0 {
180 updatesNeeded = append(updatesNeeded, id)
181 }
182 }
183 return
184 }
185
186 func createMenuPropSpec() map[string]map[string]*prop.Prop {
187 instance.menuLock.Lock()
188 defer instance.menuLock.Unlock()
189 return map[string]map[string]*prop.Prop{
190 "com.canonical.dbusmenu": {
191 "Version": {
192 Value: instance.menuVersion,
193 Writable: true,
194 Emit: prop.EmitTrue,
195 Callback: nil,
196 },
197 "TextDirection": {
198 Value: "ltr",
199 Writable: false,
200 Emit: prop.EmitTrue,
201 Callback: nil,
202 },
203 "Status": {
204 Value: "normal",
205 Writable: false,
206 Emit: prop.EmitTrue,
207 Callback: nil,
208 },
209 "IconThemePath": {
210 Value: []string{},
211 Writable: false,
212 Emit: prop.EmitTrue,
213 Callback: nil,
214 },
215 },
216 }
217 }
218
219 // menuLayout is a named struct to map into generated bindings. It represents the layout of a menu item
220 type menuLayout = struct {
221 V0 int32 // the unique ID of this item
222 V1 map[string]dbus.Variant // properties for this menu item layout
223 V2 []dbus.Variant // child menu item layouts
224 }
225
226 func addOrUpdateMenuItem(item *MenuItem) {
227 var layout *menuLayout
228 var parentForChildrenDisplayUpdate *menuLayout
229 instance.menuLock.Lock()
230 defer instance.menuLock.Unlock()
231 m, exists := findLayout(int32(item.id))
232 if exists {
233 layout = m
234 } else {
235 layout = &menuLayout{
236 V0: int32(item.id),
237 V1: map[string]dbus.Variant{},
238 V2: []dbus.Variant{},
239 }
240
241 parent := instance.menu
242 if item.parent != nil {
243 m, ok := findLayout(int32(item.parent.id))
244 if ok {
245 parent = m
246 if _, already := parent.V1["children-display"]; !already {
247 parent.V1["children-display"] = dbus.MakeVariant("submenu")
248 if parent.V0 != 0 {
249 parentForChildrenDisplayUpdate = parent
250 }
251 }
252 }
253 }
254 parent.V2 = append(parent.V2, dbus.MakeVariant(layout))
255 }
256
257 applyItemToLayout(item, layout)
258 if exists {
259 emitItemPropertiesUpdated(int32(item.id), layout.V1)
260 // Property-only change on an existing item; per spec LayoutUpdated isn't required here,
261 // but kept as a fallback for clients that only watch LayoutUpdated.
262 refresh()
263 } else {
264 // We've added "children-display", that's a property change
265 if parentForChildrenDisplayUpdate != nil {
266 emitItemPropertiesUpdated(parentForChildrenDisplayUpdate.V0, parentForChildrenDisplayUpdate.V1)
267 }
268 // New item appended to a parent's children,
269 // that's a structural change, so LayoutUpdated signal is required
270 refresh()
271 }
272 }
273
274 func addSeparator(id uint32, parent uint32) {
275 menu, _ := findLayout(int32(parent))
276
277 instance.menuLock.Lock()
278 defer instance.menuLock.Unlock()
279 layout := &menuLayout{
280 V0: int32(id),
281 V1: map[string]dbus.Variant{
282 "type": dbus.MakeVariant("separator"),
283 },
284 V2: []dbus.Variant{},
285 }
286 menu.V2 = append(menu.V2, dbus.MakeVariant(layout))
287 refresh()
288 }
289
290 // dbusKeyNames maps the platform neutral key names used by SetShortcut to the
291 // key names that the dbusmenu specification expects.
292 var dbusKeyNames = map[string]string{
293 "BackSpace": "BackSpace",
294 "Delete": "Delete",
295 "Down": "Down",
296 "End": "End",
297 "Enter": "KP_Enter",
298 "Escape": "Escape",
299 "F1": "F1",
300 "F2": "F2",
301 "F3": "F3",
302 "F4": "F4",
303 "F5": "F5",
304 "F6": "F6",
305 "F7": "F7",
306 "F8": "F8",
307 "F9": "F9",
308 "F10": "F10",
309 "F11": "F11",
310 "F12": "F12",
311 "Home": "Home",
312 "Insert": "Insert",
313 "Left": "Left",
314 "PageDown": "Page_Down",
315 "PageUp": "Page_Up",
316 "Return": "Return",
317 "Right": "Right",
318 "Space": "space",
319 "Tab": "Tab",
320 "Up": "Up",
321 }
322
323 // shortcutForItem returns the dbusmenu "shortcut" value for an item, which is a list of
324 // key presses where each press is a list of modifier names followed by the key itself.
325 func shortcutForItem(in *MenuItem) [][]string {
326 if in.shortcutKey == "" {
327 return nil
328 }
329
330 var keys []string
331 if in.shortcutMods&KeyModifierControl != 0 {
332 keys = append(keys, "Control")
333 }
334 if in.shortcutMods&KeyModifierAlt != 0 {
335 keys = append(keys, "Alt")
336 }
337 if in.shortcutMods&KeyModifierShift != 0 {
338 keys = append(keys, "Shift")
339 }
340 if in.shortcutMods&KeyModifierSuper != 0 {
341 keys = append(keys, "Super")
342 }
343
344 key, ok := dbusKeyNames[in.shortcutKey]
345 if !ok {
346 if len(in.shortcutKey) > 1 {
347 log.Printf("systray error: unsupported key %q for menu shortcut\n", in.shortcutKey)
348 return nil
349 }
350 key = strings.ToLower(in.shortcutKey)
351 }
352
353 return [][]string{append(keys, key)}
354 }
355
356 func applyItemToLayout(in *MenuItem, out *menuLayout) {
357 out.V1["enabled"] = dbus.MakeVariant(!in.disabled)
358 out.V1["label"] = dbus.MakeVariant(in.title)
359
360 if shortcut := shortcutForItem(in); shortcut != nil {
361 out.V1["shortcut"] = dbus.MakeVariant(shortcut)
362 } else {
363 delete(out.V1, "shortcut")
364 }
365
366 if in.isCheckable {
367 out.V1["toggle-type"] = dbus.MakeVariant("checkmark")
368 if in.checked {
369 out.V1["toggle-state"] = dbus.MakeVariant(1)
370 } else {
371 out.V1["toggle-state"] = dbus.MakeVariant(0)
372 }
373 } else {
374 out.V1["toggle-type"] = dbus.MakeVariant("")
375 out.V1["toggle-state"] = dbus.MakeVariant(0)
376 }
377 }
378
379 func findLayout(id int32) (*menuLayout, bool) {
380 if id == 0 {
381 return instance.menu, true
382 }
383 return findSubLayout(id, instance.menu.V2)
384 }
385
386 func findSubLayout(id int32, vals []dbus.Variant) (*menuLayout, bool) {
387 for _, i := range vals {
388 item := i.Value().(*menuLayout)
389 if item.V0 == id {
390 return item, true
391 }
392
393 if len(item.V2) > 0 {
394 child, ok := findSubLayout(id, item.V2)
395 if ok {
396 return child, true
397 }
398 }
399 }
400
401 return nil, false
402 }
403
404 func removeSubLayout(id int32, vals []dbus.Variant) ([]dbus.Variant, bool) {
405 for idx, i := range vals {
406 item := i.Value().(*menuLayout)
407 if item.V0 == id {
408 return append(vals[:idx], vals[idx+1:]...), true
409 }
410
411 if len(item.V2) > 0 {
412 if child, removed := removeSubLayout(id, item.V2); removed {
413 return child, true
414 }
415 }
416 }
417
418 return vals, false
419 }
420
421 func removeMenuItem(item *MenuItem) {
422 instance.menuLock.Lock()
423 defer instance.menuLock.Unlock()
424
425 parent := instance.menu
426 if item.parent != nil {
427 m, ok := findLayout(int32(item.parent.id))
428 if !ok {
429 return
430 }
431 parent = m
432 }
433
434 if items, removed := removeSubLayout(int32(item.id), parent.V2); removed {
435 parent.V2 = items
436 refresh()
437 }
438 }
439
440 func hideMenuItem(item *MenuItem) {
441 instance.menuLock.Lock()
442 defer instance.menuLock.Unlock()
443 m, exists := findLayout(int32(item.id))
444 if exists {
445 m.V1["visible"] = dbus.MakeVariant(false)
446 emitItemPropertiesUpdated(int32(item.id), m.V1)
447 // Property-only change; per spec LayoutUpdated isn't required here,
448 // but kept as a fallback for clients that only watch LayoutUpdated.
449 refresh()
450 }
451 }
452
453 func showMenuItem(item *MenuItem) {
454 instance.menuLock.Lock()
455 defer instance.menuLock.Unlock()
456 m, exists := findLayout(int32(item.id))
457 if exists {
458 m.V1["visible"] = dbus.MakeVariant(true)
459 emitItemPropertiesUpdated(int32(item.id), m.V1)
460 // Property-only change; per spec LayoutUpdated isn't required here,
461 // but kept as a fallback for clients that only watch LayoutUpdated.
462 refresh()
463 }
464 }
465
466 // emitItemPropertiesUpdated emits the com.canonical.dbusmenu.ItemsPropertiesUpdated
467 // signal so desktop clients refresh per-item state (label, enabled, toggle-state,
468 // visible, icon-data) without re-querying the whole layout.
469 func emitItemPropertiesUpdated(id int32, props map[string]dbus.Variant) {
470 instance.lock.Lock()
471 conn := instance.conn
472 instance.lock.Unlock()
473 if conn == nil {
474 return
475 }
476 err := menu.Emit(conn, &menu.Dbusmenu_ItemsPropertiesUpdatedSignal{
477 Path: menuPath,
478 Body: &menu.Dbusmenu_ItemsPropertiesUpdatedSignalBody{
479 UpdatedProps: []struct {
480 V0 int32
481 V1 map[string]dbus.Variant
482 }{{V0: id, V1: props}},
483 },
484 })
485 if err != nil {
486 log.Printf("systray error: failed to emit items properties updated signal: %v\n", err)
487 }
488 }
489
490 func refresh() {
491 instance.lock.Lock()
492 defer instance.lock.Unlock()
493 if instance.conn == nil || instance.menuProps == nil {
494 return
495 }
496 instance.menuVersion++
497 dbusErr := instance.menuProps.Set("com.canonical.dbusmenu", "Version",
498 dbus.MakeVariant(instance.menuVersion))
499 if dbusErr != nil {
500 log.Printf("systray error: failed to update menu version: %v\n", dbusErr)
501 return
502 }
503 err := menu.Emit(instance.conn, &menu.Dbusmenu_LayoutUpdatedSignal{
504 Path: menuPath,
505 Body: &menu.Dbusmenu_LayoutUpdatedSignalBody{
506 Revision: instance.menuVersion,
507 },
508 })
509 if err != nil {
510 log.Printf("systray error: failed to emit layout updated signal: %v\n", err)
511 }
512 }
513
514 func resetMenu() {
515 instance.menuLock.Lock()
516 defer instance.menuLock.Unlock()
517 instance.menu = &menuLayout{}
518 instance.menuVersion++
519 firstGetLayoutDone = false
520 refresh()
521 }
522
522 lines GO