1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
/*
Package systray is a cross platfrom Go library to place an icon and menu in the
notification area.
Supports Windows, Mac OSX and Linux currently.
Methods can be called from any goroutine except Run(), which should be called
at the very beginning of main() to lock at main thread.
*/
package systray
import (
"runtime"
"sync"
"sync/atomic"
"github.com/getlantern/golog"
)
var (
hasStarted = int64(0)
hasQuit = int64(0)
)
// MenuItem is used to keep track each menu item of systray
// Don't create it directly, use the one systray.AddMenuItem() returned
type MenuItem struct {
// ClickedCh is the channel which will be notified when the menu item is clicked
ClickedCh chan struct{}
// id uniquely identify a menu item, not supposed to be modified
id int32
// title is the text shown on menu item
title string
// tooltip is the text shown when pointing to menu item
tooltip string
// disabled menu item is grayed out and has no effect when clicked
disabled bool
// checked menu item has a tick before the title
checked bool
}
var (
log = golog.LoggerFor("systray")
systrayReady func()
systrayExit func()
menuItems = make(map[int32]*MenuItem)
menuItemsLock sync.RWMutex
currentID = int32(-1)
)
// Run initializes GUI and starts the event loop, then invokes the onReady
// callback.
// It blocks until systray.Quit() is called.
// Should be called at the very beginning of main() to lock at main thread.
func Run(onReady func(), onExit func()) {
runtime.LockOSThread()
atomic.StoreInt64(&hasStarted, 1)
if onReady == nil {
systrayReady = func() {}
} else {
// Run onReady on separate goroutine to avoid blocking event loop
readyCh := make(chan interface{})
go func() {
<-readyCh
onReady()
}()
systrayReady = func() {
close(readyCh)
}
}
// unlike onReady, onExit runs in the event loop to make sure it has time to
// finish before the process terminates
if onExit == nil {
onExit = func() {}
}
systrayExit = onExit
nativeLoop()
}
// Quit the systray
func Quit() {
if atomic.LoadInt64(&hasStarted) == 1 && atomic.CompareAndSwapInt64(&hasQuit, 0, 1) {
quit()
}
}
// AddMenuItem adds menu item with designated title and tooltip, returning a channel
// that notifies whenever that menu item is clicked.
//
// It can be safely invoked from different goroutines.
func AddMenuItem(title string, tooltip string) *MenuItem {
id := atomic.AddInt32(¤tID, 1)
item := &MenuItem{nil, id, title, tooltip, false, false}
item.ClickedCh = make(chan struct{})
item.update()
return item
}
// AddSeparator adds a separator bar to the menu
func AddSeparator() {
addSeparator(atomic.AddInt32(¤tID, 1))
}
// SetTitle set the text to display on a menu item
func (item *MenuItem) SetTitle(title string) {
item.title = title
item.update()
}
// SetTooltip set the tooltip to show when mouse hover
func (item *MenuItem) SetTooltip(tooltip string) {
item.tooltip = tooltip
item.update()
}
// Disabled checkes if the menu item is disabled
func (item *MenuItem) Disabled() bool {
return item.disabled
}
// Enable a menu item regardless if it's previously enabled or not
func (item *MenuItem) Enable() {
item.disabled = false
item.update()
}
// Disable a menu item regardless if it's previously disabled or not
func (item *MenuItem) Disable() {
item.disabled = true
item.update()
}
// Hide hides a menu item
func (item *MenuItem) Hide() {
hideMenuItem(item)
}
// Show shows a previously hidden menu item
func (item *MenuItem) Show() {
showMenuItem(item)
}
// Checked returns if the menu item has a check mark
func (item *MenuItem) Checked() bool {
return item.checked
}
// Check a menu item regardless if it's previously checked or not
func (item *MenuItem) Check() {
item.checked = true
item.update()
}
// Uncheck a menu item regardless if it's previously unchecked or not
func (item *MenuItem) Uncheck() {
item.checked = false
item.update()
}
// update propogates changes on a menu item to systray
func (item *MenuItem) update() {
menuItemsLock.Lock()
defer menuItemsLock.Unlock()
menuItems[item.id] = item
addOrUpdateMenuItem(item)
}
func systrayMenuItemSelected(id int32) {
menuItemsLock.RLock()
item := menuItems[id]
menuItemsLock.RUnlock()
select {
case item.ClickedCh <- struct{}{}:
// in case no one waiting for the channel
default:
}
}
|