返回 DeepSeek-Reasonix
systray_unix.go
根目录 / desktop / third_party / systray / systray_unix.go
1 //go:build (linux || freebsd || openbsd || netbsd) && !android
2
3 //Note that you need to have github.com/knightpp/dbus-codegen-go installed from "custom" branch
4 //go:generate dbus-codegen-go -prefix org.kde -package notifier -output internal/generated/notifier/status_notifier_item.go internal/StatusNotifierItem.xml
5 //go:generate dbus-codegen-go -prefix com.canonical -package menu -output internal/generated/menu/dbus_menu.go internal/DbusMenu.xml
6
7 package systray
8
9 import (
10 "bytes"
11 "fmt"
12 "image"
13 _ "image/png" // used only here
14 "log"
15 "os"
16 "sync"
17
18 "github.com/godbus/dbus/v5"
19 "github.com/godbus/dbus/v5/introspect"
20 "github.com/godbus/dbus/v5/prop"
21
22 "fyne.io/systray/internal/generated/menu"
23 "fyne.io/systray/internal/generated/notifier"
24 )
25
26 const (
27 path = "/StatusNotifierItem"
28 menuPath = "/StatusNotifierItem/menu"
29 )
30
31 var (
32 // to signal quitting the internal main loop
33 quitChan = make(chan struct{})
34
35 // instance is the current instance of our DBus tray server
36 instance = &tray{menu: &menuLayout{}, menuVersion: 1}
37 )
38
39 // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
40 // to a regular icon on other platforms.
41 // templateIconBytes and iconBytes should be the content of .ico for windows and
42 // .ico/.jpg/.png for other platforms.
43 func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
44 // TODO handle the templateIconBytes?
45 SetIcon(regularIconBytes)
46 }
47
48 // SetIcon sets the systray icon.
49 // iconBytes should be the content of .ico for windows and .ico/.jpg/.png
50 // for other platforms.
51 func SetIcon(iconBytes []byte) {
52 instance.lock.Lock()
53 instance.iconData = iconBytes
54 props := instance.props
55 conn := instance.conn
56 defer instance.lock.Unlock()
57
58 if props == nil {
59 return
60 }
61
62 props.SetMust("org.kde.StatusNotifierItem", "IconPixmap",
63 []PX{convertToPixels(iconBytes)})
64 if conn == nil {
65 return
66 }
67
68 err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewIconSignal{
69 Path: path,
70 Body: &notifier.StatusNotifierItem_NewIconSignalBody{},
71 })
72 if err != nil {
73 log.Printf("systray error: failed to emit new icon signal: %s\n", err)
74 return
75 }
76 }
77
78 // SetIconFromFilePath sets the systray icon from a file path.
79 // iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms.
80 func SetIconFromFilePath(iconFilePath string) error {
81 bytes, err := os.ReadFile(iconFilePath)
82 if err != nil {
83 return fmt.Errorf("failed to read icon file: %v", err)
84 }
85 SetIcon(bytes)
86 return nil
87 }
88
89 // SetTitle sets the systray title, only available on Mac and Linux.
90 func SetTitle(t string) {
91 instance.lock.Lock()
92 instance.title = t
93 props := instance.props
94 conn := instance.conn
95 defer instance.lock.Unlock()
96
97 if props == nil {
98 return
99 }
100 dbusErr := props.Set("org.kde.StatusNotifierItem", "Title",
101 dbus.MakeVariant(t))
102 if dbusErr != nil {
103 log.Printf("systray error: failed to set Title prop: %s\n", dbusErr)
104 return
105 }
106
107 if conn == nil {
108 return
109 }
110
111 err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewTitleSignal{
112 Path: path,
113 Body: &notifier.StatusNotifierItem_NewTitleSignalBody{},
114 })
115 if err != nil {
116 log.Printf("systray error: failed to emit new title signal: %s\n", err)
117 return
118 }
119 }
120
121 // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
122 // only available on Mac and Windows.
123 func SetTooltip(tooltipTitle string) {
124 instance.lock.Lock()
125 instance.tooltipTitle = tooltipTitle
126 props := instance.props
127 conn := instance.conn
128 defer instance.lock.Unlock()
129
130 if props == nil {
131 return
132 }
133 dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip",
134 dbus.MakeVariant(tooltip{V2: tooltipTitle}))
135 if dbusErr != nil {
136 log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr)
137 return
138 }
139
140 if conn == nil {
141 return
142 }
143
144 err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewToolTipSignal{
145 Path: path,
146 Body: &notifier.StatusNotifierItem_NewToolTipSignalBody{},
147 })
148 if err != nil {
149 log.Printf("systray error: failed to emit new tooltip signal: %s\n", err)
150 return
151 }
152 }
153
154 // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and
155 // Linux, it falls back to the regular icon bytes.
156 // templateIconBytes and regularIconBytes should be the content of .ico for windows and
157 // .ico/.jpg/.png for other platforms.
158 func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
159 item.SetIcon(regularIconBytes)
160 }
161
162 // SetRemovalAllowed sets whether a user can remove the systray icon or not.
163 // This is only supported on macOS.
164 func SetRemovalAllowed(allowed bool) {
165 }
166
167 func setInternalLoop(_ bool) {
168 // nothing to action on Linux
169 }
170
171 func registerSystray() {
172 }
173
174 func nativeLoop() int {
175 nativeStart()
176 <-quitChan
177 nativeEnd()
178 return 0
179 }
180
181 func nativeEnd() {
182 runSystrayExit()
183 if conn := instance.conn; conn != nil {
184 conn.Close()
185 }
186 }
187
188 func quit() {
189 close(quitChan)
190 }
191
192 func nativeStart() {
193 systrayReady()
194 conn, err := dbus.SessionBus()
195 if err != nil {
196 log.Printf("systray error: failed to connect to DBus: %v\n", err)
197 return
198 }
199 err = notifier.ExportStatusNotifierItem(conn, path, newLeftRightNotifierItem())
200 if err != nil {
201 log.Printf("systray error: failed to export status notifier item: %v\n", err)
202 }
203 err = menu.ExportDbusmenu(conn, menuPath, instance)
204 if err != nil {
205 log.Printf("systray error: failed to export status notifier menu: %v\n", err)
206 return
207 }
208
209 name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process
210 _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue)
211 if err != nil {
212 log.Printf("systray error: failed to request name: %s\n", err)
213 // it's not critical error: continue
214 }
215 props, err := prop.Export(conn, path, instance.createPropSpec())
216 if err != nil {
217 log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err)
218 return
219 }
220 menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec())
221 if err != nil {
222 log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err)
223 return
224 }
225
226 node := introspect.Node{
227 Name: path,
228 Interfaces: []introspect.Interface{
229 introspect.IntrospectData,
230 prop.IntrospectData,
231 notifier.IntrospectDataStatusNotifierItem,
232 },
233 }
234 err = conn.Export(introspect.NewIntrospectable(&node), path,
235 "org.freedesktop.DBus.Introspectable")
236 if err != nil {
237 log.Printf("systray error: failed to export node introspection: %s\n", err)
238 return
239 }
240 menuNode := introspect.Node{
241 Name: menuPath,
242 Interfaces: []introspect.Interface{
243 introspect.IntrospectData,
244 prop.IntrospectData,
245 menu.IntrospectDataDbusmenu,
246 },
247 }
248 err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath,
249 "org.freedesktop.DBus.Introspectable")
250 if err != nil {
251 log.Printf("systray error: failed to export menu node introspection: %s\n", err)
252 return
253 }
254
255 instance.lock.Lock()
256 instance.conn = conn
257 instance.props = props
258 instance.menuProps = menuProps
259 instance.lock.Unlock()
260
261 go stayRegistered()
262 }
263
264 func register() bool {
265 obj := instance.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
266 call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path)
267 if call.Err != nil {
268 log.Printf("systray error: failed to register: %v\n", call.Err)
269 return false
270 }
271
272 return true
273 }
274
275 func stayRegistered() {
276 register()
277
278 conn := instance.conn
279 if err := conn.AddMatchSignal(
280 dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
281 dbus.WithMatchInterface("org.freedesktop.DBus"),
282 dbus.WithMatchSender("org.freedesktop.DBus"),
283 dbus.WithMatchMember("NameOwnerChanged"),
284 dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"),
285 ); err != nil {
286 log.Printf("systray error: failed to register signal matching: %v\n", err)
287 // If we can't monitor signals, there is no point in
288 // us being here. we're either registered or not (per
289 // above) and will roll the dice from here...
290 return
291 }
292
293 sc := make(chan *dbus.Signal, 10)
294 conn.Signal(sc)
295
296 for {
297 select {
298 case sig := <-sc:
299 if sig == nil {
300 return // We get a nil signal when closing the window.
301 } else if len(sig.Body) < 3 {
302 return // malformed signal?
303 }
304
305 // sig.Body has the args, which are [name old_owner new_owner]
306 if s, ok := sig.Body[2].(string); ok && s != "" {
307 register()
308 }
309 case <-quitChan:
310 return
311 }
312 }
313 }
314
315 // tray is a basic type that handles the dbus functionality
316 type tray struct {
317 // the DBus connection that we will use
318 conn *dbus.Conn
319
320 // icon data for the main systray icon
321 iconData []byte
322 // title and tooltip state
323 title, tooltipTitle string
324
325 lock sync.Mutex
326 menu *menuLayout
327 menuLock sync.RWMutex
328 props, menuProps *prop.Properties
329 menuVersion uint32
330 }
331
332 func (t *tray) createPropSpec() map[string]map[string]*prop.Prop {
333 t.lock.Lock()
334 defer t.lock.Unlock()
335 id := t.title
336 if id == "" {
337 id = fmt.Sprintf("systray_%d", os.Getpid())
338 }
339 return map[string]map[string]*prop.Prop{
340 "org.kde.StatusNotifierItem": {
341 "Status": {
342 Value: "Active", // Passive, Active or NeedsAttention
343 Writable: false,
344 Emit: prop.EmitTrue,
345 Callback: nil,
346 },
347 "Title": {
348 Value: t.title,
349 Writable: true,
350 Emit: prop.EmitTrue,
351 Callback: nil,
352 },
353 "Id": {
354 Value: id,
355 Writable: false,
356 Emit: prop.EmitTrue,
357 Callback: nil,
358 },
359 "Category": {
360 Value: "ApplicationStatus",
361 Writable: false,
362 Emit: prop.EmitTrue,
363 Callback: nil,
364 },
365 "IconName": {
366 Value: "",
367 Writable: false,
368 Emit: prop.EmitTrue,
369 Callback: nil,
370 },
371 "IconPixmap": {
372 Value: []PX{convertToPixels(t.iconData)},
373 Writable: true,
374 Emit: prop.EmitTrue,
375 Callback: nil,
376 },
377 "IconThemePath": {
378 Value: "",
379 Writable: false,
380 Emit: prop.EmitTrue,
381 Callback: nil,
382 },
383 "ItemIsMenu": {
384 Value: tappedLeft == nil && tappedRight == nil,
385 Writable: false,
386 Emit: prop.EmitTrue,
387 Callback: nil,
388 },
389 "Menu": {
390 Value: dbus.ObjectPath(menuPath),
391 Writable: true,
392 Emit: prop.EmitTrue,
393 Callback: nil,
394 },
395 "ToolTip": {
396 Value: tooltip{V2: t.tooltipTitle},
397 Writable: true,
398 Emit: prop.EmitTrue,
399 Callback: nil,
400 },
401 "WindowId": {
402 Value: int32(0), // 0 means "not interested" per the SNI spec
403 Writable: false,
404 Emit: prop.EmitTrue,
405 Callback: nil,
406 },
407 "OverlayIconName": {
408 Value: "",
409 Writable: false,
410 Emit: prop.EmitTrue,
411 Callback: nil,
412 },
413 "OverlayIconPixmap": {
414 Value: []PX{},
415 Writable: false,
416 Emit: prop.EmitTrue,
417 Callback: nil,
418 },
419 "AttentionIconName": {
420 Value: "",
421 Writable: false,
422 Emit: prop.EmitTrue,
423 Callback: nil,
424 },
425 "AttentionIconPixmap": {
426 Value: []PX{},
427 Writable: false,
428 Emit: prop.EmitTrue,
429 Callback: nil,
430 },
431 "AttentionMovieName": {
432 Value: "",
433 Writable: false,
434 Emit: prop.EmitTrue,
435 Callback: nil,
436 },
437 }}
438 }
439
440 // PX is picture pix map structure with width and high
441 type PX struct {
442 W, H int
443 Pix []byte
444 }
445
446 // tooltip is our data for a tooltip property.
447 // Param names need to match the generated code...
448 type tooltip = struct {
449 V0 string // name
450 V1 []PX // icons
451 V2 string // title
452 V3 string // description
453 }
454
455 func convertToPixels(data []byte) PX {
456 if len(data) == 0 {
457 return PX{}
458 }
459
460 img, _, err := image.Decode(bytes.NewReader(data))
461 if err != nil {
462 log.Printf("Failed to read icon format %v", err)
463 return PX{}
464 }
465
466 return PX{
467 img.Bounds().Dx(), img.Bounds().Dy(),
468 argbForImage(img),
469 }
470 }
471
472 func argbForImage(img image.Image) []byte {
473 w, h := img.Bounds().Dx(), img.Bounds().Dy()
474 data := make([]byte, w*h*4)
475 i := 0
476 for y := 0; y < h; y++ {
477 for x := 0; x < w; x++ {
478 r, g, b, a := img.At(x, y).RGBA()
479 data[i] = byte(a)
480 data[i+1] = byte(r)
481 data[i+2] = byte(g)
482 data[i+3] = byte(b)
483 i += 4
484 }
485 }
486 return data
487 }
488
488 lines GO