forked from spyoungtech/FreeSimpleGUI
-
Notifications
You must be signed in to change notification settings - Fork 0
/
psgtray.py
282 lines (236 loc) · 11.8 KB
/
psgtray.py
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import pystray, io, base64, threading, time
from PIL import Image
import FreeSimpleGUI as sg
"""
A System Tray Icon implementation that can work with the tkinter port of PySimpleGUI!
To use, add this import to your code:
from psgtray import SystemTray
Make sure the psgtray.py file is in the same folder as your app or is on your Python path
Because this code is entirely in the user's space it's possible to use the pystray package
to implement the system tray icon feature. You need to install pystray and PIL.
As of this date, the latest version of pystray is 0.17.3
This code works well under Windows.
On Linux there are some challenges. Some changes were
needed in order to get pystray to run as a thread using gtk as the backend.
The separator '---' caused problems so it is now ignored. Unknown why it caused the
menu to not show at all, but it does.
A sample bit of code is at the bottom for your reference.
Your window will receive events from the system tray thread.
In addition to the init, these are the class methods available:
change_icon
hide_icon
show_icon
set_tooltip
notify
In your code, you will receive events from tray with key SystemTray.key
The value will be the choice made or a click event. This is the magic statement:
window.write_event_value(tray.key, item.text)
Extra Special thanks to FireDM for the design pattern that made this work.
(https://github.com/firedm/FireDM)
Copyright 2021 PySimpleGUI
"""
class SystemTray:
DOUBLE_CLICK_THRESHOLD = 500 # time in milliseconds to determine double clicks
DEFAULT_KEY = '-TRAY-' # the default key that will be used to send events to your window
key_counter = 0
def __init__(self, menu=None, icon=None, tooltip='', single_click_events=False, window=None, key=DEFAULT_KEY):
"""
A System Tray Icon
Initializing the object is all that is required to make the tray icon and start the thread.
:param menu: The PySimpleGUI menu data structure
:type menu: List[List[Tuple[str, List[str]]]
:param icon: Icon to show in the system tray. Can be a file or a BASE64 byte string
:type icon: str | bytes
:param tooltip: Tooltip that is shown when mouse hovers over the system tray icon
:type tooltip: str
:param single_click_events: If True then both single click and double click events will be generated
:type single_click_events: bool
:param window: The window where the events will be sent using window.write_event_value
:type window: sg.Window
"""
self.title = tooltip
self.tray_icon = None # type: pystray.Icon
self.window = window
self.tooltip = tooltip
self.menu_items = self._convert_psg_menu_to_tray(menu[1])
self.key = key if SystemTray.key_counter == 0 else key+str(SystemTray.key_counter)
SystemTray.key_counter += 1
self.double_click_timer = 0
self.single_click_events_enabled = single_click_events
if icon is None:
self.icon = sg.DEFAULT_BASE64_ICON
else:
self.icon = icon
self.thread_started = False
self.thread = threading.Thread(target=self._pystray_thread, daemon=True)
self.thread.start()
while not self.thread_started: # wait for the thread to begin
time.sleep(.2)
time.sleep(.2) # one more slight delay to allow things to actually get running
def change_icon(self, icon=None):
"""
Change the icon shown in the tray to a file or a BASE64 byte string.
:param icon: The icon to change to
:type icon: str | bytes
"""
if icon is not None:
self.tray_icon.icon = self._create_image(icon)
def hide_icon(self):
"""
Hides the icon
"""
self.tray_icon.visible = False
def show_icon(self):
"""
Shows a previously hidden icon
"""
self.tray_icon.visible = True
def set_tooltip(self, tooltip):
"""
Set the tooltip that is shown when hovering over the icon in the system tray
"""
self.tray_icon.title = tooltip
def show_message(self, title=None, message=None):
"""
Show a notification message balloon in the system tray
:param title: Title that is shown at the top of the balloon
:type title: str
:param message: Main message to be displayed
:type message: str
"""
self.tray_icon.notify(title=str(title) if title is not None else '', message=str(message) if message is not None else '')
def close(self):
"""
Whlie not required, calling close will remove the icon from the tray right away.
"""
self.tray_icon.visible = False # hiding will close any message bubbles that may hold up the removal of icon from tray
self.tray_icon.stop()
# --------------------------- The methods below this point are not meant to be user callable ---------------------------
def _on_clicked(self, icon, item: pystray.MenuItem):
self.window.write_event_value(self.key, item.text)
def _convert_psg_menu_to_tray(self, psg_menu):
menu_items = []
i = 0
if isinstance(psg_menu, list):
while i < len(psg_menu):
item = psg_menu[i]
look_ahead = item
if i != (len(psg_menu) - 1):
look_ahead = psg_menu[i + 1]
if not isinstance(item, list) and not isinstance(look_ahead, list):
disabled = False
if item == sg.MENU_SEPARATOR_LINE:
item = pystray.Menu.SEPARATOR
elif item.startswith(sg.MENU_DISABLED_CHARACTER):
disabled = True
item = item[1:]
if not (item == pystray.Menu.SEPARATOR and sg.running_linux()):
menu_items.append(pystray.MenuItem(item, self._on_clicked, enabled=not disabled, default=False))
elif look_ahead != item:
if isinstance(look_ahead, list):
if menu_items is None:
menu_items = pystray.MenuItem(item, pystray.Menu(*self._convert_psg_menu_to_tray(look_ahead)))
else:
menu_items.append(pystray.MenuItem(item, pystray.Menu(*self._convert_psg_menu_to_tray(look_ahead))))
i += 1
# important item - this is where clicking the icon itself will go
menu_items.append(pystray.MenuItem('default', self._default_action_callback, enabled=True, default=True, visible=False))
return menu_items
def _default_action_callback(self):
delta = (time.time() - self.double_click_timer) * 1000
if delta < self.DOUBLE_CLICK_THRESHOLD: # if last click was recent, then this click is a double-click
self.window.write_event_value(self.key, sg.EVENT_SYSTEM_TRAY_ICON_DOUBLE_CLICKED)
self.double_click_timer = 0
else:
if self.single_click_events_enabled:
self.window.write_event_value(self.key, sg.EVENT_SYSTEM_TRAY_ICON_ACTIVATED)
self.double_click_timer = time.time()
def _pystray_thread(self):
self.tray_icon = pystray.Icon(self.title, self._create_image(self.icon))
self.tray_icon.default_action = self._default_action_callback
self.tray_icon.menu = pystray.Menu(*self.menu_items)
self.tray_icon.title = self.tooltip # tooltip for the icon
self.thread_started = True
self.tray_icon.run()
def _create_image(self, icon):
if isinstance(icon, bytes):
buffer = io.BytesIO(base64.b64decode(icon))
img = Image.open(buffer)
elif isinstance(icon, str):
img = Image.open(icon)
else:
img = None
return img
# MM""""""""`M dP
# MM mmmmmmmM 88
# M` MMMM dP. .dP .d8888b. 88d8b.d8b. 88d888b. 88 .d8888b.
# MM MMMMMMMM `8bd8' 88' `88 88'`88'`88 88' `88 88 88ooood8
# MM MMMMMMMM .d88b. 88. .88 88 88 88 88. .88 88 88. ...
# MM .M dP' `dP `88888P8 dP dP dP 88Y888P' dP `88888P'
# MMMMMMMMMMMM 88
# dP
# M""MMMMM""M
# M MMMMM M
# M MMMMM M .d8888b. .d8888b.
# M MMMMM M Y8ooooo. 88ooood8
# M `MMM' M 88 88. ...
# Mb dM `88888P' `88888P'
# MMMMMMMMMMM
def main():
# This example shows using TWO tray icons together
menu = ['', ['Show Window', 'Hide Window', '---', '!Disabled Item', 'Change Icon', ['Happy', 'Sad', 'Plain'], 'Exit']]
tooltip = 'Tooltip'
layout = [[sg.Text('My PySimpleGUI Window with a Tray Icon - X will minimize to tray')],
[sg.Text('Note - you are running a file that is meant to be imported')],
[sg.T('Change Icon Tooltip:'), sg.Input(tooltip, key='-IN-', s=(20,1)), sg.B('Change Tooltip')],
[sg.Multiline(size=(60,10), reroute_stdout=False, reroute_cprint=True, write_only=True, key='-OUT-')],
[sg.Button('Go'), sg.B('Hide Icon'), sg.B('Show Icon'), sg.B('Hide Window'), sg.B('Close Tray'), sg.Button('Exit')]]
window = sg.Window('Window Title', layout, finalize=True, enable_close_attempted_event=True)
tray1 = SystemTray(menu, single_click_events=False, window=window, tooltip=tooltip, icon=sg.DEFAULT_BASE64_ICON)
tray2 = SystemTray(menu, single_click_events=False, window=window, tooltip=tooltip, icon=sg.EMOJI_BASE64_HAPPY_JOY)
time.sleep(.5) # wait just a little bit since TWO are being started at once
tray2.show_message('Started', 'Both tray icons started')
while True:
event, values = window.read()
print(event, values)
# IMPORTANT step. It's not required, but convenient.
# if it's a tray event, change the event variable to be whatever the tray sent
# This will make your event loop homogeneous with event conditionals all using the same event variable
if event in (tray1.key, tray2.key):
sg.cprint(f'System Tray Event = ', values[event], c='white on red')
tray = tray1 if event == tray1.key else tray2
event = values[event] # use the System Tray's event as if was from the window
else:
tray = tray1 # if wasn't a tray event, there's still a tray varaible used, so default to "first" tray created
if event in (sg.WIN_CLOSED, 'Exit'):
break
sg.cprint(event, values)
tray.show_message(title=event, message=values)
tray.set_tooltip(values['-IN-'])
if event in ('Show Window', sg.EVENT_SYSTEM_TRAY_ICON_DOUBLE_CLICKED):
window.un_hide()
window.bring_to_front()
elif event in ('Hide Window', sg.WIN_CLOSE_ATTEMPTED_EVENT):
window.hide()
tray.show_icon() # if hiding window, better make sure the icon is visible
# tray.notify('System Tray Item Chosen', f'You chose {event}')
elif event == 'Happy':
tray.change_icon(sg.EMOJI_BASE64_HAPPY_JOY)
elif event == 'Sad':
tray.change_icon(sg.EMOJI_BASE64_FRUSTRATED)
elif event == 'Plain':
tray.change_icon(sg.DEFAULT_BASE64_ICON)
elif event == 'Hide Icon':
tray.hide_icon()
elif event == 'Show Icon':
tray.show_icon()
elif event == 'Close Tray':
tray.close()
elif event == 'Change Tooltip':
tray.set_tooltip(values['-IN-'])
tray1.close()
tray2.close()
window.close()
if __name__ == '__main__':
# Normally this file is not "run"
main()