~jconti/recent-notifications/trunk

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