1"""This is a pure-python replacement for notify-python, using python-dbus 2to communicate with the notifications server directly. It's compatible with 3Python 2 and 3, and its callbacks can work with Gtk 3 or Qt 4 applications. 4 5To use it, first call ``notify2.init('app name')``, then create and show notifications:: 6 7 n = notify2.Notification("Summary", 8 "Some body text", 9 "notification-message-im" # Icon name 10 ) 11 n.show() 12 13API docs are `available on ReadTheDocs <https://notify2.readthedocs.org/en/latest/>`_, 14or you can refer to docstrings. 15 16Based on the notifications spec at: 17http://developer.gnome.org/notification-spec/ 18 19Porting applications from pynotify 20---------------------------------- 21 22There are a few differences from pynotify you should be aware of: 23 24- If you need callbacks from notifications, notify2 must know about your event 25 loop. The simplest way is to pass 'glib' or 'qt' as the ``mainloop`` parameter 26 to ``init``. 27- The methods ``attach_to_widget`` and ``attach_to_status_icon`` are not 28 implemented. You can calculate the location you want the notification to 29 appear and call ``Notification``. 30- ``set_property`` and ``get_property`` are not implemented. The summary, body 31 and icon are accessible as attributes of a ``Notification`` instance. 32- Various methods that pynotify Notification instances got from gobject do not 33 exist, or only implement part of the functionality. 34 35Several pynotify functions, especially getters and setters, are only supported 36for compatibility. You are encouraged to use more direct, Pythonic alternatives. 37""" 38 39import dbus 40 41__version__ = '0.3.1' 42 43# Constants 44EXPIRES_DEFAULT = -1 45EXPIRES_NEVER = 0 46 47URGENCY_LOW = 0 48URGENCY_NORMAL = 1 49URGENCY_CRITICAL = 2 50urgency_levels = [URGENCY_LOW, URGENCY_NORMAL, URGENCY_CRITICAL] 51 52# Initialise the module (following pynotify's API) ----------------------------- 53 54initted = False 55appname = "" 56_have_mainloop = False 57 58class UninittedError(RuntimeError): 59 """Error raised if you try to communicate with the server before calling 60 :func:`init`. 61 """ 62 pass 63 64class UninittedDbusObj(object): 65 def __getattr__(self, name): 66 raise UninittedError("You must call notify2.init() before using the " 67 "notification features.") 68 69dbus_iface = UninittedDbusObj() 70 71def init(app_name, mainloop=None): 72 """Initialise the D-Bus connection. Must be called before you send any 73 notifications, or retrieve server info or capabilities. 74 75 To get callbacks from notifications, DBus must be integrated with a mainloop. 76 There are three ways to achieve this: 77 78 - Set a default mainloop (dbus.set_default_main_loop) before calling init() 79 - Pass the mainloop parameter as a string 'glib' or 'qt' to integrate with 80 those mainloops. (N.B. passing 'qt' currently makes that the default dbus 81 mainloop, because that's the only way it seems to work.) 82 - Pass the mainloop parameter a DBus compatible mainloop instance, such as 83 dbus.mainloop.glib.DBusGMainLoop(). 84 85 If you only want to display notifications, without receiving information 86 back from them, you can safely omit mainloop. 87 """ 88 global appname, initted, dbus_iface, _have_mainloop 89 90 if mainloop == 'glib': 91 from dbus.mainloop.glib import DBusGMainLoop 92 mainloop = DBusGMainLoop() 93 elif mainloop == 'qt': 94 from dbus.mainloop.qt import DBusQtMainLoop 95 # For some reason, this only works if we make it the default mainloop 96 # for dbus. That might make life tricky for anyone trying to juggle two 97 # event loops, but I can't see any way round it. 98 mainloop = DBusQtMainLoop(set_as_default=True) 99 100 bus = dbus.SessionBus(mainloop=mainloop) 101 102 dbus_obj = bus.get_object('org.freedesktop.Notifications', 103 '/org/freedesktop/Notifications') 104 dbus_iface = dbus.Interface(dbus_obj, 105 dbus_interface='org.freedesktop.Notifications') 106 appname = app_name 107 initted = True 108 109 if mainloop or dbus.get_default_main_loop(): 110 _have_mainloop = True 111 dbus_iface.connect_to_signal('ActionInvoked', _action_callback) 112 dbus_iface.connect_to_signal('NotificationClosed', _closed_callback) 113 114 return True 115 116def is_initted(): 117 """Has init() been called? Only exists for compatibility with pynotify. 118 """ 119 return initted 120 121def get_app_name(): 122 """Return appname. Only exists for compatibility with pynotify. 123 """ 124 return appname 125 126def uninit(): 127 """Undo what init() does.""" 128 global initted, dbus_iface, _have_mainloop 129 initted = False 130 _have_mainloop = False 131 dbus_iface = UninittedDbusObj() 132 133# Retrieve basic server information -------------------------------------------- 134 135def get_server_caps(): 136 """Get a list of server capabilities. 137 138 These are short strings, listed `in the spec <http://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html#commands>`_. 139 Vendors may also list extra capabilities with an 'x-' prefix, e.g. 'x-canonical-append'. 140 """ 141 return [str(x) for x in dbus_iface.GetCapabilities()] 142 143def get_server_info(): 144 """Get basic information about the server. 145 """ 146 res = dbus_iface.GetServerInformation() 147 return {'name': str(res[0]), 148 'vendor': str(res[1]), 149 'version': str(res[2]), 150 'spec-version': str(res[3]), 151 } 152 153# Action callbacks ------------------------------------------------------------- 154 155notifications_registry = {} 156 157def _action_callback(nid, action): 158 nid, action = int(nid), str(action) 159 try: 160 n = notifications_registry[nid] 161 except KeyError: 162 #this message was created through some other program. 163 return 164 n._action_callback(action) 165 166def _closed_callback(nid, reason): 167 nid, reason = int(nid), int(reason) 168 try: 169 n = notifications_registry[nid] 170 except KeyError: 171 #this message was created through some other program. 172 return 173 n._closed_callback(n) 174 del notifications_registry[nid] 175 176def no_op(*args): 177 """No-op function for callbacks. 178 """ 179 pass 180 181# Controlling notifications ---------------------------------------------------- 182 183ActionsDictClass = dict # fallback for old version of Python 184try: 185 from collections import OrderedDict 186 ActionsDictClass = OrderedDict 187except ImportError: 188 pass 189 190 191class Notification(object): 192 """A notification object. 193 194 summary : str 195 The title text 196 message : str 197 The body text, if the server has the 'body' capability. 198 icon : str 199 Path to an icon image, or the name of a stock icon. Stock icons available 200 in Ubuntu are `listed here <https://wiki.ubuntu.com/NotificationDevelopmentGuidelines#How_do_I_get_these_slick_icons>`_. 201 You can also set an icon from data in your application - see 202 :meth:`set_icon_from_pixbuf`. 203 """ 204 id = 0 205 timeout = -1 # -1 = server default settings 206 _closed_callback = no_op 207 208 def __init__(self, summary, message='', icon=''): 209 self.summary = summary 210 self.message = message 211 self.icon = icon 212 self.hints = {} 213 self.actions = ActionsDictClass() 214 self.data = {} # Any data the user wants to attach 215 216 def show(self): 217 """Ask the server to show the notification. 218 219 Call this after you have finished setting any parameters of the 220 notification that you want. 221 """ 222 nid = dbus_iface.Notify(appname, # app_name (spec names) 223 self.id, # replaces_id 224 self.icon, # app_icon 225 self.summary, # summary 226 self.message, # body 227 self._make_actions_array(), # actions 228 self.hints, # hints 229 self.timeout, # expire_timeout 230 ) 231 232 self.id = int(nid) 233 234 if _have_mainloop: 235 notifications_registry[self.id] = self 236 return True 237 238 def update(self, summary, message="", icon=None): 239 """Replace the summary and body of the notification, and optionally its 240 icon. You should call :meth:`show` again after this to display the 241 updated notification. 242 """ 243 self.summary = summary 244 self.message = message 245 if icon is not None: 246 self.icon = icon 247 248 def close(self): 249 """Ask the server to close this notification.""" 250 if self.id != 0: 251 dbus_iface.CloseNotification(self.id) 252 253 def set_hint(self, key, value): 254 """n.set_hint(key, value) <--> n.hints[key] = value 255 256 See `hints in the spec <http://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html#hints>`_. 257 258 Only exists for compatibility with pynotify. 259 """ 260 self.hints[key] = value 261 262 set_hint_string = set_hint_int32 = set_hint_double = set_hint 263 264 def set_hint_byte(self, key, value): 265 """Set a hint with a dbus byte value. The input value can be an 266 integer or a bytes string of length 1. 267 """ 268 self.hints[key] = dbus.Byte(value) 269 270 def set_urgency(self, level): 271 """Set the urgency level to one of URGENCY_LOW, URGENCY_NORMAL or 272 URGENCY_CRITICAL. 273 """ 274 if level not in urgency_levels: 275 raise ValueError("Unknown urgency level specified", level) 276 self.set_hint_byte("urgency", level) 277 278 def set_category(self, category): 279 """Set the 'category' hint for this notification. 280 281 See `categories in the spec <http://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html#categories>`_. 282 """ 283 self.hints['category'] = category 284 285 def set_timeout(self, timeout): 286 """Set the display duration in milliseconds, or one of the special 287 values EXPIRES_DEFAULT or EXPIRES_NEVER. This is a request, which the 288 server might ignore. 289 290 Only exists for compatibility with pynotify; you can simply set:: 291 292 n.timeout = 5000 293 """ 294 if not isinstance(timeout, int): 295 raise TypeError("timeout value was not int", timeout) 296 self.timeout = timeout 297 298 def get_timeout(self): 299 """Return the timeout value for this notification. 300 301 Only exists for compatibility with pynotify; you can inspect the 302 timeout attribute directly. 303 """ 304 return self.timeout 305 306 def add_action(self, action, label, callback, user_data=None): 307 """Add an action to the notification. 308 309 Check for the 'actions' server capability before using this. 310 311 action : str 312 A brief key. 313 label : str 314 The text displayed on the action button 315 callback : callable 316 A function taking at 2-3 parameters: the Notification object, the 317 action key and (if specified) the user_data. 318 user_data : 319 An extra argument to pass to the callback. 320 """ 321 self.actions[action] = (label, callback, user_data) 322 323 def _make_actions_array(self): 324 """Make the actions array to send over DBus. 325 """ 326 arr = [] 327 for action, (label, callback, user_data) in self.actions.items(): 328 arr.append(action) 329 arr.append(label) 330 return arr 331 332 def _action_callback(self, action): 333 """Called when the user selects an action on the notification, to 334 dispatch it to the relevant user-specified callback. 335 """ 336 try: 337 label, callback, user_data = self.actions[action] 338 except KeyError: 339 return 340 341 if user_data is None: 342 callback(self, action) 343 else: 344 callback(self, action, user_data) 345 346 def connect(self, event, callback): 347 """Set the callback for the notification closing; the only valid value 348 for event is 'closed' (the parameter is kept for compatibility with pynotify). 349 350 The callback will be called with the :class:`Notification` instance. 351 """ 352 if event != 'closed': 353 raise ValueError("'closed' is the only valid value for event", event) 354 self._closed_callback = callback 355 356 def set_data(self, key, value): 357 """n.set_data(key, value) <--> n.data[key] = value 358 359 Only exists for compatibility with pynotify. 360 """ 361 self.data[key] = value 362 363 def get_data(self, key): 364 """n.get_data(key) <--> n.data[key] 365 366 Only exists for compatibility with pynotify. 367 """ 368 return self.data[key] 369 370 def set_icon_from_pixbuf(self, icon): 371 """Set a custom icon from a GdkPixbuf. 372 """ 373 struct = ( 374 icon.get_width(), 375 icon.get_height(), 376 icon.get_rowstride(), 377 icon.get_has_alpha(), 378 icon.get_bits_per_sample(), 379 icon.get_n_channels(), 380 dbus.ByteArray(icon.get_pixels()) 381 ) 382 self.hints['icon_data'] = struct 383 384 def set_location(self, x, y): 385 """Set the notification location as (x, y), if the server supports it. 386 """ 387 if (not isinstance(x, int)) or (not isinstance(y, int)): 388 raise TypeError("x and y must both be ints", (x,y)) 389 self.hints['x'] = x 390 self.hints['y'] = y 391 392