175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
1 |
"""
|
2 |
Notification.py
|
|
3 |
by Jason Conti
|
|
4 |
February 19, 2010
|
|
5 |
updated: March 26, 2011
|
|
6 |
||
7 |
Monitors DBUS for org.freedesktop.Notifications.Notify messages, parses them,
|
|
8 |
and notifies listeners when they arrive.
|
|
9 |
"""
|
|
10 |
||
11 |
import dbus |
|
12 |
import logging |
|
186
by Jason Conti
Introducing an ugly hack to load image_data until GdkPixbuf.Pixbuf.new_from_data is fixed. |
13 |
import os |
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
14 |
|
15 |
from dbus.mainloop.glib import DBusGMainLoop |
|
186
by Jason Conti
Introducing an ugly hack to load image_data until GdkPixbuf.Pixbuf.new_from_data is fixed. |
16 |
from gi.repository import GdkPixbuf, GObject, Gtk |
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
17 |
|
18 |
import Icon |
|
19 |
import Timestamp |
|
20 |
||
21 |
logger = logging.getLogger("Notification") |
|
22 |
||
23 |
class ImageDataException(Exception): |
|
24 |
pass
|
|
25 |
||
26 |
class ImageData(object): |
|
27 |
"""Parses the image_data hint from a DBUS message."""
|
|
28 |
def __init__(self, image_data): |
|
29 |
if len(image_data) < 7: |
|
30 |
raise ImageDataException("Invalid image_data: " + repr(image_data)) |
|
31 |
||
32 |
self.width = int(image_data[0]) |
|
33 |
self.height = int(image_data[1]) |
|
34 |
self.rowstride = int(image_data[2]) |
|
35 |
self.has_alpha = bool(image_data[3]) |
|
36 |
self.bits_per_sample = int(image_data[4]) |
|
37 |
self.channels = int(image_data[5]) |
|
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
38 |
self.data = self.dbus_array_to_int(image_data[6]) |
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
39 |
|
40 |
def dbus_array_to_str(self, array): |
|
41 |
return "".join(map(chr, array)) |
|
42 |
||
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
43 |
def dbus_array_to_int(self, array): |
44 |
return map(int, array) |
|
45 |
||
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
46 |
def get_pixbuf(self, size): |
47 |
"""Creates a pixbuf from the image data and scale it to the appropriate size."""
|
|
188
by Jason Conti
The ugly hack to load image_data was still broken, rather than fix it, I just removed the support for image_data again for the moment. For now, the app tries to load the icon from a lowercase version of the app_name, and if that fails, then it will load the generic icon. Should work for Pidgin and Gwibber, which are the only two that send image_data that I use daily at the moment. |
48 |
pixbuf = GdkPixbuf.Pixbuf.new_from_data(self.data, GdkPixbuf.Colorspace.RGB, |
49 |
self.has_alpha, self.bits_per_sample, self.width, self.height, |
|
50 |
self.rowstride, lambda *args: None, None) |
|
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
51 |
|
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
52 |
copy_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, self.has_alpha, self.bits_per_sample, |
53 |
self.width, self.height) |
|
54 |
pixbuf.copy_area(0, 0, self.width, self.height, copy_pixbuf, 0, 0) |
|
55 |
||
56 |
return copy_pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) |
|
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
57 |
|
58 |
class MessageException(Exception): |
|
59 |
pass
|
|
60 |
||
61 |
class Message(GObject.GObject): |
|
62 |
"""Parses a DBUS message in the Notify format specified at:
|
|
63 |
http://www.galago-project.org/specs/notification/0.9/index.html"""
|
|
64 |
# Message urgency
|
|
65 |
LOW = 0 |
|
66 |
NORMAL = 1 |
|
67 |
CRITICAL = 2 |
|
68 |
X_CANONICAL_PRIVATE_SYNCHRONOUS = "x-canonical-private-synchronous" |
|
69 |
def __init__(self, dbus_message = None, timestamp = None): |
|
70 |
GObject.GObject.__init__(self) |
|
71 |
||
72 |
self.timestamp = timestamp |
|
73 |
||
74 |
args = dbus_message.get_args_list() |
|
75 |
||
76 |
if len(args) != 8: |
|
77 |
raise MessageException("Invalid message args_list: " + repr(args)) |
|
78 |
||
79 |
self.app_name = str(args[0]) |
|
80 |
self.replaces_id = args[1] |
|
81 |
self.app_icon = str(args[2]) |
|
178
by Jason Conti
Worked around a bug with encodings. Set wrap width for MessageList on size-allocate events. Clear the unread status and count on focus-out-event. |
82 |
self.summary = unicode(args[3]) |
83 |
self.body = unicode(args[4]) |
|
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
84 |
self.actions = args[5] |
85 |
self.hints = dict(args[6]) |
|
86 |
self.expire_timeout = args[7] |
|
87 |
||
88 |
if "urgency" in self.hints: |
|
89 |
urgency = self.hints["urgency"] |
|
90 |
if urgency == 0: |
|
91 |
self.urgency = Message.LOW |
|
92 |
elif urgency == 2: |
|
93 |
self.urgency = Message.CRITICAL |
|
94 |
else: |
|
95 |
self.urgency = Message.NORMAL |
|
96 |
else: |
|
97 |
self.urgency = Message.NORMAL |
|
98 |
||
99 |
if "image_data" in self.hints: |
|
100 |
self.image_data = ImageData(self.hints["image_data"]) |
|
101 |
else: |
|
102 |
self.image_data = None |
|
103 |
||
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
104 |
if "icon_data" in self.hints: |
105 |
self.icon_data = ImageData(self.hints["icon_data"]) |
|
106 |
else: |
|
107 |
self.icon_data = None |
|
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
108 |
|
109 |
self.log_message() |
|
110 |
||
111 |
def get_icon(self, size = 48): |
|
112 |
"""Loads the icon into a pixbuf. Adapted from the load_icon code in
|
|
113 |
bubble.c of notify-osd."""
|
|
114 |
icon_name = self.app_icon |
|
115 |
||
116 |
# Try to load the pixbuf from a file
|
|
117 |
if icon_name.startswith("file://") or icon_name.startswith("/"): |
|
118 |
icon = Icon.load_from_file(icon_name, size) |
|
119 |
if icon != None: |
|
120 |
return icon |
|
121 |
||
122 |
# Try to load the pixbuf from the current icon theme
|
|
123 |
elif icon_name != "": |
|
124 |
icon = Icon.load(icon_name, size) |
|
125 |
if icon != None: |
|
126 |
return icon |
|
127 |
||
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
128 |
# Try to load the icon data from the message
|
129 |
elif self.icon_data != None: |
|
130 |
return self.icon_data.get_pixbuf(size) |
|
131 |
||
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
132 |
# Try to load the image data from the message
|
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
133 |
elif self.image_data != None: |
134 |
return self.image_data.get_pixbuf(size) |
|
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
135 |
|
189
by Jason Conti
Mostly implemented filtering. No decrement on the combobox yet. Reverted the icon by app_name, was throwing a seemingly uncatchable GError. |
136 |
return self.get_default_icon(size) |
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
137 |
|
138 |
def get_default_icon(self, size = 48): |
|
139 |
"""Attempts to load the default message icon, returns None on failure."""
|
|
140 |
if self.urgency == Message.LOW: |
|
141 |
return Icon.load("notification-low", size) |
|
142 |
elif self.urgency == Message.CRITICAL: |
|
143 |
return Icon.load("notification-critical", size) |
|
144 |
else: |
|
145 |
return Icon.load("notification-normal", size) |
|
146 |
||
147 |
def is_volume_notification(self): |
|
148 |
"""Returns true if this is a volume message. The volume notifications
|
|
149 |
in Ubuntu are a special case, and mostly a blank message. Clutters up
|
|
150 |
the display and provides no useful information, so it is better to
|
|
151 |
discard them."""
|
|
152 |
if Message.X_CANONICAL_PRIVATE_SYNCHRONOUS in self.hints: |
|
153 |
return str(self.hints[Message.X_CANONICAL_PRIVATE_SYNCHRONOUS]) == "volume" |
|
154 |
||
155 |
def log_message(self): |
|
156 |
"""Write debug info about a message."""
|
|
157 |
result = [ |
|
158 |
"-" * 50, |
|
159 |
"Message created at: " + Timestamp.locale_datetime(self.timestamp), |
|
160 |
"app_name: " + repr(self.app_name), |
|
161 |
"replaces_id: " + repr(self.replaces_id), |
|
162 |
"app_icon: " + repr(self.app_icon), |
|
163 |
"summary: " + repr(self.summary), |
|
164 |
"body: " + repr(self.body), |
|
165 |
"actions: " + repr(self.actions), |
|
166 |
"expire_timeout: " + repr(self.expire_timeout), |
|
167 |
"hints:"
|
|
168 |
]
|
|
169 |
||
170 |
# Log all the hints except image_data
|
|
171 |
for key in self.hints: |
|
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
172 |
if key not in ["icon_data", "image_data"]: |
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
173 |
result.append(" " + str(key) + ": " + repr(self.hints[key])) |
174 |
||
192
by Jason Conti
GdkPixbuf.Pixbuf.new_from_data is partially fixed with the latest update in Natty, however the pixbufs are extremely corrupted unless copied to a freshly allocated pixbuf first. Using that workaround for now, and reenabled icons from image_data. |
175 |
# Log info about icon_data
|
176 |
if self.icon_data != None: |
|
177 |
result.append("icon_data:") |
|
178 |
result.append(" width: " + repr(self.icon_data.width)) |
|
179 |
result.append(" height: " + repr(self.icon_data.height)) |
|
180 |
result.append(" rowstride: " + repr(self.icon_data.rowstride)) |
|
181 |
result.append(" has_alpha: " + repr(self.icon_data.has_alpha)) |
|
182 |
result.append(" bits_per_sample: " + repr(self.icon_data.bits_per_sample)) |
|
183 |
result.append(" channels: " + repr(self.icon_data.channels)) |
|
184 |
||
175
by Jason Conti
Ported Notification.py to gobject introspection, everything except dbus. Going to attempt that next, but may revert if it doesn't work so well yet. |
185 |
# Log info about the image_data
|
186 |
if self.image_data != None: |
|
187 |
result.append("image_data:") |
|
188 |
result.append(" width: " + repr(self.image_data.width)) |
|
189 |
result.append(" height: " + repr(self.image_data.height)) |
|
190 |
result.append(" rowstride: " + repr(self.image_data.rowstride)) |
|
191 |
result.append(" has_alpha: " + repr(self.image_data.has_alpha)) |
|
192 |
result.append(" bits_per_sample: " + repr(self.image_data.bits_per_sample)) |
|
193 |
result.append(" channels: " + repr(self.image_data.channels)) |
|
194 |
||
195 |
result.append("-" * 50) |
|
196 |
||
197 |
logger.debug("\n" + "\n".join(result)) |
|
198 |
||
199 |
class Notification(GObject.GObject): |
|
200 |
"""Monitors DBUS for org.freedesktop.Notifications.Notify messages, parses them,
|
|
201 |
and notifies listeners when they arrive."""
|
|
202 |
__gsignals__ = { |
|
203 |
"message-received": (GObject.SignalFlags.RUN_LAST, None, [Message]) |
|
204 |
}
|
|
205 |
def __init__(self): |
|
206 |
GObject.GObject.__init__(self) |
|
207 |
||
208 |
self._blacklist = None |
|
209 |
self._match_string = "type='method_call',interface='org.freedesktop.Notifications',member='Notify'" |
|
210 |
||
211 |
DBusGMainLoop(set_as_default=True) |
|
212 |
self._bus = dbus.SessionBus() |
|
213 |
self._bus.add_match_string(self._match_string) |
|
214 |
self._bus.add_message_filter(self._message_filter) |
|
215 |
||
216 |
def close(self): |
|
217 |
"""Closes the connection to the session bus."""
|
|
218 |
self._bus.close() |
|
219 |
||
220 |
def set_blacklist(self, blacklist): |
|
221 |
"""Defines the set of app_names of messages to be discarded."""
|
|
222 |
self._blacklist = blacklist |
|
223 |
||
224 |
def _message_filter(self, connection, dbus_message): |
|
225 |
"""Triggers when messages are received from the session bus."""
|
|
226 |
if dbus_message.get_member() == "Notify" and dbus_message.get_interface() == "org.freedesktop.Notifications": |
|
227 |
try: |
|
228 |
message = Message(dbus_message, Timestamp.now()) |
|
229 |
except: |
|
230 |
logger.exception("Failed to parse dbus message: " + repr(dbus_message.get_args_list())) |
|
231 |
else: |
|
232 |
# Discard unwanted messages
|
|
233 |
if message.is_volume_notification(): |
|
234 |
return
|
|
235 |
if self._blacklist and self._blacklist.get_bool(message.app_name, False): |
|
236 |
return
|
|
237 |
logger.debug("Sending message-received") |
|
238 |
self.emit("message-received", message) |
|
239 |
||
240 |
GObject.type_register(Message) |
|
241 |
GObject.type_register(Notification) |
|
242 |