SysNotify is a simple, pure python notification module for use with PyQt5 desktop applications. It uses python-dbus to communicate directly with the systems notification server, and supports callback from notification actions via the pyqt5 dbus main loop. It provides basically the same features as the gtk-2.0 pynotify library, but does not require any GTK libraries, so avoids the dependency hell which can be a problem with Gtk. It is similar to and based on notify2, but is simplified and updated to support PyQt5.
If python-dbus.mainloop.pyqt5
is not installed any notification action
callbacks will not work, but the notifications will still be shown.
Depends on:
- python-dbus
- python-dbus.mainloop.pyqt5 (optional)
Tested on:
- Debian 9
- Ubuntu 18.04
Example Notification
Code
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
#!/usr/bin/env python
import dbus
from collections import OrderedDict
DBusQtMainLoop = None
try:
from dbus.mainloop.pyqt5 import DBusQtMainLoop
except:
print "Could not import DBusQtMainLoop, is package 'python-dbus.mainloop.pyqt5' installed?"
APP_NAME = ''
DBUS_IFACE = None
NOTIFICATIONS = {}
class Urgency:
"""freedesktop.org notification urgency levels"""
LOW, NORMAL, CRITICAL = range(3)
class UninitializedError(RuntimeError):
"""Error raised if you try to show an error before initializing"""
pass
def init(app_name):
"""Initializes the DBus connection"""
global APP_NAME, DBUS_IFACE
APP_NAME = app_name
name = "org.freedesktop.Notifications"
path = "/org/freedesktop/Notifications"
interface = "org.freedesktop.Notifications"
mainloop = None
if DBusQtMainLoop is not None:
mainloop = DBusQtMainLoop(set_as_default=True)
bus = dbus.SessionBus(mainloop)
proxy = bus.get_object(name, path)
DBUS_IFACE = dbus.Interface(proxy, interface)
if mainloop is not None:
# We have a mainloop, so connect callbacks
DBUS_IFACE.connect_to_signal('ActionInvoked', _onActionInvoked)
DBUS_IFACE.connect_to_signal('NotificationClosed', _onNotificationClosed)
def _onActionInvoked(nid, action):
"""Called when a notification action is clicked"""
nid, action = int(nid), str(action)
try:
notification = NOTIFICATIONS[nid]
except KeyError:
# must have been created by some other program
return
notification._onActionInvoked(action)
def _onNotificationClosed(nid, reason):
"""Called when the notification is closed"""
nid, reason = int(nid), int(reason)
try:
notification = NOTIFICATIONS[nid]
except KeyError:
# must have been created by some other program
return
notification._onNotificationClosed(notification)
del NOTIFICATIONS[nid]
class Notification(object):
"""Notification object"""
id = 0
timeout = -1
_onNotificationClosed = lambda *args: None
def __init__(self, title, body='', icon='', timeout=-1):
"""Initializes a new notification object.
Args:
title (str): The title of the notification
body (str, optional): The body text of the notification
icon (str, optional): The icon to display with the notification
timeout (TYPE, optional): The time in ms before the notification hides, -1 for default, 0 for never
"""
self.title = title # title of the notification
self.body = body # the body text of the notification
self.icon = icon # the path to the icon to use
self.timeout = timeout # time in ms before the notification disappears
self.hints = {} # dict of various display hints
self.actions = OrderedDict() # actions names and their callbacks
self.data = {} # arbitrary user data
def show(self):
if DBUS_IFACE is None:
raise UninitializedError("You must call 'notify.init()' before 'notify.show()'")
"""Asks the notification server to show the notification"""
nid = DBUS_IFACE.Notify(APP_NAME,
self.id,
self.icon,
self.title,
self.body,
self._makeActionsList(),
self.hints,
self.timeout,
)
self.id = int(nid)
NOTIFICATIONS[self.id] = self
return True
def close(self):
"""Ask the notification server to close the notification"""
if self.id != 0:
DBUS_IFACE.CloseNotification(self.id)
def onClosed(self, callback):
"""Set the callback called when the notification is closed"""
self._onNotificationClosed = callback
def setUrgency(self, value):
"""Set the freedesktop.org notification urgency level"""
if value not in range(3):
raise ValueError("Unknown urgency level '%s' specified" % level)
self.hints['urgency'] = dbus.Byte(value)
def setSoundFile(self, sound_file):
"""Sets a sound file to play when the notification shows"""
self.hints['sound-file'] = sound_file
def setSoundName(self, sound_name):
"""Set a freedesktop.org sound name to play when notification shows"""
self.hints['sound-name'] = sound_name
def setIconPath(self, icon_path):
"""Set the URI of the icon to display in the notification"""
self.hints['image-path'] = 'file://' + icon_path
def setQIcon(self, q_icon):
# FixMe this would be convenient, but may not be possible
raise NotImplemented
def setLocation(self, x_pos, y_pos):
"""Sets the location to display the notification"""
self.hints['x'] = int(x_pos)
self.hints['y'] = int(y_pos)
def setCategory(self, category):
"""Sets the the freedesktop.org notification category"""
self.hints['category'] = category
def setTimeout(self, timeout):
"""Set the display duration in milliseconds, -1 for default"""
if not isinstance(timeout, int):
raise TypeError("Timeout value '%s' was not int" % timeout)
self.timeout = timeout
def setHint(self, key, value):
"""Set one of the other hints"""
self.hints[key] = value
def addAction(self, action, label, callback, user_data=None):
"""Add an action to the notification.
Args:
action (str): A sort key identifying the action
label (str): The text to display on the action button
callback (bound method): The method to call when the action is activated
user_data (any, optional): Any user data to be passed to the action callback
"""
self.actions[action] = (label, callback, user_data)
def _makeActionsList(self):
"""Make the actions array to send over DBus"""
arr = []
for action, (label, callback, user_data) in self.actions.items():
arr.append(action)
arr.append(label)
return arr
def _onActionInvoked(self, action):
"""Called when the user activates a notification action"""
try:
label, callback, user_data = self.actions[action]
except KeyError:
return
if user_data is None:
callback(self, action)
else:
callback(self, action, user_data)
# ----------------------- E X A M P L E -----------------------
def onHelp(n, action):
assert(action == "help"), "Action was not help!"
print "You clicked Help action"
n.close()
def onIgnore(n, action, data):
assert(action == "ignore"), "Action was not ignore!"
print "You clicked Ignore action"
print "Passed user data was: ", data
n.close()
def onClose(n):
print "Notification closed"
app.quit()
if __name__ == "__main__":
import sys
from PyQt5.QtCore import QCoreApplication
app = QCoreApplication(sys.argv)
# Initialize the DBus connection to the notification server
init("demo")
# Initialize a new notification object
n = Notification("Demo Notification",
"This notification is very important as it " +
"notifies you that notifications are working.",
timeout=3000
)
n.setUrgency(Urgency.NORMAL)
n.setCategory("device")
n.setIconPath("/usr/share/icons/Tango/scalable/status/dialog-error.svg")
# no user data
n.addAction("help", "Help", onHelp)
# passing arbitrary user data to the callback
n.addAction("ignore", "Ignore", onIgnore, 12345)
n.onClosed(onClose)
n.show()
app.exec_()
SysNotify can be downloaded from the GitHub Gist located here.