返回 DeepSeek-Reasonix
tray.go
根目录 / desktop / tray.go
1 //go:build windows || cgo
2
3 package main
4
5 import (
6 "context"
7 goruntime "runtime"
8 "sync"
9
10 "fyne.io/systray"
11 )
12
13 type desktopTray struct {
14 end func()
15 openItem *systray.MenuItem
16 quitItem *systray.MenuItem
17 once sync.Once
18 ready chan struct{}
19 readyOnce sync.Once
20 healthMu sync.Mutex
21 cancel context.CancelFunc
22 healthStopped bool
23 }
24
25 func newDesktopTray() *desktopTray {
26 return &desktopTray{ready: make(chan struct{})}
27 }
28
29 func (t *desktopTray) markReady() {
30 t.readyOnce.Do(func() {
31 close(t.ready)
32 })
33 }
34
35 func (a *App) startTray() bool {
36 if a == nil || a.shuttingDown.Load() || a.forceQuit.Load() {
37 return false
38 }
39 if a.hostShell != nil {
40 return a.hostShell.startTray()
41 }
42 if !traySupported() {
43 reason := "no_session_bus"
44 if goruntime.GOOS == "darwin" {
45 reason = "platform_no_tray"
46 }
47 a.setTrayHealth(nil, "unavailable", reason)
48 return false
49 }
50 a.mu.Lock()
51 if a.tray != nil {
52 a.mu.Unlock()
53 return true
54 }
55 t := newDesktopTray()
56 a.tray = t
57 a.desktopShell.trayState = "probing"
58 a.desktopShell.trayReason = ""
59 a.trayReady = false
60 a.mu.Unlock()
61
62 end := startDesktopTray(func() {
63 systray.SetIcon(trayIconBytes)
64 systray.SetTitle("Reasonix")
65 systray.SetTooltip("Reasonix")
66 // Run off the systray Win32 message loop: SetOnTapped fires inside wndProc,
67 // so a blocking showFromTray (a wedged webview after sleep freezes
68 // runtime.WindowShow) would stall the whole tray's message pump (#3834). The
69 // menu items below are already decoupled via goroutines for the same reason.
70 systray.SetOnTapped(func() { a.goSafe("showFromTray", a.showFromTray) })
71 // Keep secondary/right-click on systray's native menu path.
72 systray.SetOnSecondaryTapped(nil)
73
74 labels := trayMenuLabels(a.trayLocale())
75 openItem := systray.AddMenuItem(labels.openTitle, labels.openTooltip)
76 quitItem := systray.AddMenuItem(labels.quitTitle, labels.quitTooltip)
77
78 // Publish the menu items under a.mu: this callback runs on the systray
79 // goroutine while bound settings calls (updateTrayLocale) read them.
80 a.mu.Lock()
81 t.openItem = openItem
82 t.quitItem = quitItem
83 a.mu.Unlock()
84 a.trayConfigured(t)
85
86 a.goSafe("trayOpenLoop", func() {
87 for range openItem.ClickedCh {
88 a.showFromTray()
89 }
90 })
91 a.goSafe("trayQuitLoop", func() {
92 for range quitItem.ClickedCh {
93 a.quitFromTray()
94 }
95 })
96 }, func() {
97 t.stopHealthMonitor()
98 a.setTrayHealth(t, "unavailable", "tray_exited")
99 a.mu.Lock()
100 if a.tray == t {
101 a.tray = nil
102 }
103 a.mu.Unlock()
104 })
105 a.mu.Lock()
106 t.end = end
107 a.mu.Unlock()
108 if a.shuttingDown.Load() || a.forceQuit.Load() {
109 t.once.Do(end)
110 return false
111 }
112 a.startTrayHealthMonitor(t)
113 return true
114 }
115
116 func (a *App) stopTray() {
117 if a.hostShell != nil {
118 a.hostShell.stopTray()
119 return
120 }
121 a.mu.RLock()
122 t := a.tray
123 var end func()
124 if t != nil {
125 end = t.end
126 }
127 a.mu.RUnlock()
128 if t == nil || end == nil {
129 return
130 }
131 t.once.Do(end)
132 }
133
134 func (t *desktopTray) stopHealthMonitor() {
135 if t == nil {
136 return
137 }
138 t.healthMu.Lock()
139 t.healthStopped = true
140 cancel := t.cancel
141 t.cancel = nil
142 t.healthMu.Unlock()
143 if cancel != nil {
144 cancel()
145 }
146 }
147
148 func (a *App) updateTrayLocale(locale string) {
149 if a.hostShell != nil {
150 a.hostShell.updateTrayLocale(locale)
151 return
152 }
153 a.mu.RLock()
154 t := a.tray
155 var openItem, quitItem *systray.MenuItem
156 if t != nil {
157 openItem = t.openItem
158 quitItem = t.quitItem
159 }
160 a.mu.RUnlock()
161 if openItem == nil || quitItem == nil {
162 return
163 }
164 labels := trayMenuLabels(locale)
165 openItem.SetTitle(labels.openTitle)
166 openItem.SetTooltip(labels.openTooltip)
167 quitItem.SetTitle(labels.quitTitle)
168 quitItem.SetTooltip(labels.quitTooltip)
169 }
170
170 lines GO