返回 DeepSeek-Reasonix
systray.go
根目录 / desktop / third_party / systray / systray.go
1 // Package systray is a cross-platform Go library to place an icon and menu in the notification area.
2 package systray
3
4 import (
5 "fmt"
6 "log"
7 "runtime"
8 "sync"
9 "sync/atomic"
10 )
11
12 var (
13 systrayReady, systrayExit func()
14 tappedLeft, tappedRight func()
15 systrayExitCalled bool
16 menuItems = make(map[uint32]*MenuItem)
17 menuItemsLock sync.RWMutex
18
19 initialMenuBuilt sync.WaitGroup
20 currentID atomic.Uint32
21 quitOnce sync.Once
22
23 // TrayOpenedCh receives an entry each time the system tray menu is opened.
24 TrayOpenedCh = make(chan struct{})
25 )
26
27 // This helper function allows us to call systrayExit only once,
28 // without accidentally calling it twice in the same lifetime.
29 func runSystrayExit() {
30 if !systrayExitCalled {
31 systrayExitCalled = true
32 systrayExit()
33 }
34 }
35
36 func init() {
37 runtime.LockOSThread()
38 }
39
40 // KeyModifier is a bit mask of the modifier keys that form part of a menu item shortcut.
41 type KeyModifier int
42
43 const (
44 // KeyModifierShift represents the "Shift" key.
45 KeyModifierShift KeyModifier = 1 << iota
46 // KeyModifierControl represents the "Control" key.
47 KeyModifierControl
48 // KeyModifierAlt represents the "Alt" key (also known as "Option" on macOS).
49 KeyModifierAlt
50 // KeyModifierSuper represents the "Super" key (also known as "Command" on macOS
51 // and "Windows" on Microsoft Windows).
52 KeyModifierSuper
53 )
54
55 // MenuItem is used to keep track each menu item of systray.
56 // Don't create it directly, use the one systray.AddMenuItem() returned
57 type MenuItem struct {
58 // ClickedCh is the channel which will be notified when the menu item is clicked
59 ClickedCh chan struct{}
60
61 // id uniquely identify a menu item, not supposed to be modified
62 id uint32
63 // title is the text shown on menu item
64 title string
65 // tooltip is the text shown when pointing to menu item
66 tooltip string
67 // disabled menu item is grayed out and has no effect when clicked
68 disabled bool
69 // checked menu item has a tick before the title
70 checked bool
71 // has the menu item a checkbox (Linux)
72 isCheckable bool
73 // shortcutKey is the key of the keyboard shortcut for this item, if any
74 shortcutKey string
75 // shortcutMods are the modifier keys of the keyboard shortcut for this item
76 shortcutMods KeyModifier
77 // parent item, for sub menus
78 parent *MenuItem
79 }
80
81 func (item *MenuItem) String() string {
82 if item.parent == nil {
83 return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title)
84 }
85 return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title)
86 }
87
88 // newMenuItem returns a populated MenuItem object
89 func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem {
90 item := &MenuItem{
91 ClickedCh: make(chan struct{}),
92 id: currentID.Add(1),
93 title: title,
94 tooltip: tooltip,
95 disabled: false,
96 checked: false,
97 isCheckable: false,
98 parent: parent,
99 }
100
101 menuItemsLock.Lock()
102 menuItems[item.id] = item
103 menuItemsLock.Unlock()
104
105 return item
106 }
107
108 // Run initializes GUI and starts the event loop, then invokes the onReady
109 // callback. It blocks until systray.Quit() is called.
110 func Run(onReady, onExit func()) {
111 setInternalLoop(true)
112 Register(onReady, onExit)
113
114 nativeLoop()
115 }
116
117 // RunWithExternalLoop allows the system tray module to operate with other toolkits.
118 // The returned start and end functions should be called by the toolkit when the application has started and will end.
119 func RunWithExternalLoop(onReady, onExit func()) (start, end func()) {
120 Register(onReady, onExit)
121
122 return nativeStart, func() {
123 nativeEnd()
124 Quit()
125 }
126 }
127
128 // Register initializes GUI and registers the callbacks but relies on the
129 // caller to run the event loop somewhere else. It's useful if the program
130 // needs to show other UI elements, for example, webview.
131 // To overcome some OS weirdness, On macOS versions before Catalina, calling
132 // this does exactly the same as Run().
133 func Register(onReady func(), onExit func()) {
134 if onReady == nil {
135 systrayReady = func() {}
136 } else {
137 // Run onReady on separate goroutine to avoid blocking event loop
138 readyCh := make(chan interface{})
139 initialMenuBuilt.Add(1)
140 go func() {
141 <-readyCh
142 onReady()
143 initialMenuBuilt.Done()
144 }()
145 systrayReady = func() {
146 close(readyCh)
147 }
148 }
149 // unlike onReady, onExit runs in the event loop to make sure it has time to
150 // finish before the process terminates
151 if onExit == nil {
152 onExit = func() {}
153 }
154 systrayExit = onExit
155 systrayExitCalled = false
156 registerSystray()
157 }
158
159 // ResetMenu will remove all menu items
160 func ResetMenu() {
161 menuItemsLock.Lock()
162 id := currentID.Load()
163 items := make([]*MenuItem, 0, len(menuItems))
164 for _, item := range menuItems {
165 items = append(items, item)
166 }
167 menuItemsLock.Unlock()
168 for _, item := range items {
169 if item.id <= id && item.parent == nil {
170 item.Remove()
171 }
172 }
173 resetMenu()
174 }
175
176 // Quit the systray
177 func Quit() {
178 quitOnce.Do(quit)
179 }
180
181 func SetOnTapped(f func()) {
182 tappedLeft = f
183 }
184
185 func SetOnSecondaryTapped(f func()) {
186 tappedRight = f
187 }
188
189 // AddMenuItem adds a menu item with the designated title and tooltip.
190 // It can be safely invoked from different goroutines.
191 // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox
192 func AddMenuItem(title string, tooltip string) *MenuItem {
193 item := newMenuItem(title, tooltip, nil)
194 item.update()
195 return item
196 }
197
198 // AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux.
199 // On other platforms there will be a check indicated next to the item if `checked` is true.
200 // It can be safely invoked from different goroutines.
201 func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem {
202 item := newMenuItem(title, tooltip, nil)
203 item.isCheckable = true
204 item.checked = checked
205 item.update()
206 return item
207 }
208
209 // AddSeparator adds a separator bar to the menu
210 func AddSeparator() {
211 addSeparator(currentID.Add(1), 0)
212 }
213
214 // AddSeparator adds a separator bar to the submenu
215 func (item *MenuItem) AddSeparator() {
216 addSeparator(currentID.Add(1), item.id)
217 }
218
219 // AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip.
220 // It can be safely invoked from different goroutines.
221 // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox
222 func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem {
223 child := newMenuItem(title, tooltip, item)
224 child.update()
225 return child
226 }
227
228 // AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux.
229 // It can be safely invoked from different goroutines.
230 // On Windows and OSX this is the same as calling AddSubMenuItem
231 func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem {
232 child := newMenuItem(title, tooltip, item)
233 child.isCheckable = true
234 child.checked = checked
235 child.update()
236 return child
237 }
238
239 // SetTitle set the text to display on a menu item
240 func (item *MenuItem) SetTitle(title string) {
241 item.title = title
242 item.update()
243 }
244
245 // SetTooltip set the tooltip to show when mouse hover
246 func (item *MenuItem) SetTooltip(tooltip string) {
247 item.tooltip = tooltip
248 item.update()
249 }
250
251 // SetShortcut sets the keyboard shortcut that will be displayed alongside this menu item.
252 // The key should be a single character such as "S" or one of the named keys understood by
253 // all platforms, namely "BackSpace", "Delete", "Down", "End", "Enter", "Escape", "F1" to "F12",
254 // "Home", "Insert", "Left", "PageDown", "PageUp", "Return", "Right", "Space", "Tab" and "Up".
255 // Passing an empty key removes any shortcut previously set.
256 //
257 // On macOS the shortcut will also be registered so it can trigger the item, on Linux and
258 // Windows it is presented next to the item label but not handled by the system tray.
259 func (item *MenuItem) SetShortcut(mods KeyModifier, key string) {
260 item.shortcutMods = mods
261 item.shortcutKey = key
262 item.update()
263 }
264
265 // Shortcut returns the modifiers and key of the keyboard shortcut for this menu item.
266 // An empty key means that no shortcut is set.
267 func (item *MenuItem) Shortcut() (mods KeyModifier, key string) {
268 return item.shortcutMods, item.shortcutKey
269 }
270
271 // Disabled checks if the menu item is disabled
272 func (item *MenuItem) Disabled() bool {
273 return item.disabled
274 }
275
276 // Enable a menu item regardless if it's previously enabled or not
277 func (item *MenuItem) Enable() {
278 item.disabled = false
279 item.update()
280 }
281
282 // Disable a menu item regardless if it's previously disabled or not
283 func (item *MenuItem) Disable() {
284 item.disabled = true
285 item.update()
286 }
287
288 // Hide hides a menu item
289 func (item *MenuItem) Hide() {
290 hideMenuItem(item)
291 }
292
293 // Remove removes a menu item
294 func (item *MenuItem) Remove() {
295 menuItemsLock.RLock()
296 var childList []*MenuItem
297 for _, child := range menuItems {
298 if child.parent == item {
299 childList = append(childList, child)
300 }
301 }
302 menuItemsLock.RUnlock()
303 for _, child := range childList {
304 child.Remove()
305 }
306 removeMenuItem(item)
307 menuItemsLock.Lock()
308 defer menuItemsLock.Unlock()
309 delete(menuItems, item.id)
310 if item.ClickedCh == nil {
311 return
312 }
313 select {
314 case _, ok := <-item.ClickedCh:
315 if !ok {
316 return
317 }
318 default:
319 }
320 close(item.ClickedCh)
321 }
322
323 // Show shows a previously hidden menu item
324 func (item *MenuItem) Show() {
325 showMenuItem(item)
326 }
327
328 // Checked returns if the menu item has a check mark
329 func (item *MenuItem) Checked() bool {
330 return item.checked
331 }
332
333 // Check a menu item regardless if it's previously checked or not
334 func (item *MenuItem) Check() {
335 item.checked = true
336 item.update()
337 }
338
339 // Uncheck a menu item regardless if it's previously unchecked or not
340 func (item *MenuItem) Uncheck() {
341 item.checked = false
342 item.update()
343 }
344
345 // update propagates changes on a menu item to systray
346 func (item *MenuItem) update() {
347 menuItemsLock.Lock()
348 _, exists := menuItems[item.id]
349 menuItemsLock.Unlock()
350
351 if !exists {
352 return
353 }
354 addOrUpdateMenuItem(item)
355 }
356
357 func systrayMenuItemSelected(id uint32) {
358 menuItemsLock.RLock()
359 item, ok := menuItems[id]
360 menuItemsLock.RUnlock()
361 if !ok {
362 log.Printf("systray error: no menu item with ID %d\n", id)
363 return
364 }
365 select {
366 case item.ClickedCh <- struct{}{}:
367 // in case no one waiting for the channel
368 default:
369 }
370 }
371
371 lines GO