set transient for roster windows to error / warning dialogs. Fixes #6942
[gajim.git] / src / notify.py
blobf00691495a60eb3ae01778479a6b49923f7e7f6a
1 # -*- coding:utf-8 -*-
2 ## src/notify.py
3 ##
4 ## Copyright (C) 2005 Sebastian Estienne
5 ## Copyright (C) 2005-2006 Andrew Sayman <lorien420 AT myrealbox.com>
6 ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2005-2010 Yann Leboulanger <asterix AT lagaule.org>
8 ## Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
9 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10 ## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
11 ## Stephan Erb <steve-e AT h3c.de>
12 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
13 ## Jonathan Schleifer <js-gajim AT webkeks.org>
15 ## This file is part of Gajim.
17 ## Gajim is free software; you can redistribute it and/or modify
18 ## it under the terms of the GNU General Public License as published
19 ## by the Free Software Foundation; version 3 only.
21 ## Gajim is distributed in the hope that it will be useful,
22 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ## GNU General Public License for more details.
26 ## You should have received a copy of the GNU General Public License
27 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
30 import os
31 import time
32 from dialogs import PopupNotificationWindow
33 import gobject
34 import gtkgui_helpers
35 import gtk
37 from common import gajim
38 from common import helpers
39 from common import ged
41 from common import dbus_support
42 if dbus_support.supported:
43 import dbus
44 import dbus.glib
47 USER_HAS_PYNOTIFY = True # user has pynotify module
48 try:
49 import pynotify
50 pynotify.init('Gajim Notification')
51 except ImportError:
52 USER_HAS_PYNOTIFY = False
54 def get_show_in_roster(event, account, contact, session=None):
55 """
56 Return True if this event must be shown in roster, else False
57 """
58 if event == 'gc_message_received':
59 return True
60 if event == 'message_received':
61 if session and session.control:
62 return False
63 return True
65 def get_show_in_systray(event, account, contact, type_=None):
66 """
67 Return True if this event must be shown in systray, else False
68 """
69 if type_ == 'printed_gc_msg' and not gajim.config.get(
70 'notify_on_all_muc_messages'):
71 # it's not an highlighted message, don't show in systray
72 return False
73 return gajim.config.get('trayicon_notification_on_events')
75 def popup(event_type, jid, account, msg_type='', path_to_image=None, title=None,
76 text=None):
77 """
78 Notify a user of an event. It first tries to a valid implementation of
79 the Desktop Notification Specification. If that fails, then we fall back to
80 the older style PopupNotificationWindow method
81 """
82 # default image
83 if not path_to_image:
84 path_to_image = gtkgui_helpers.get_icon_path('gajim-chat_msg_recv', 48)
86 # Try to show our popup via D-Bus and notification daemon
87 if gajim.config.get('use_notif_daemon') and dbus_support.supported:
88 try:
89 DesktopNotification(event_type, jid, account, msg_type,
90 path_to_image, title, gobject.markup_escape_text(text))
91 return # sucessfully did D-Bus Notification procedure!
92 except dbus.DBusException, e:
93 # Connection to D-Bus failed
94 gajim.log.debug(str(e))
95 except TypeError, e:
96 # This means that we sent the message incorrectly
97 gajim.log.debug(str(e))
99 # Ok, that failed. Let's try pynotify, which also uses notification daemon
100 if gajim.config.get('use_notif_daemon') and USER_HAS_PYNOTIFY:
101 if not text and event_type == 'new_message':
102 # empty text for new_message means do_preview = False
103 # -> default value for text
104 _text = gobject.markup_escape_text(
105 gajim.get_name_from_jid(account, jid))
106 else:
107 _text = gobject.markup_escape_text(text)
109 if not title:
110 _title = ''
111 else:
112 _title = title
114 notification = pynotify.Notification(_title, _text)
115 timeout = gajim.config.get('notification_timeout') * 1000 # make it ms
116 notification.set_timeout(timeout)
118 notification.set_category(event_type)
119 notification.set_data('event_type', event_type)
120 notification.set_data('jid', jid)
121 notification.set_data('account', account)
122 notification.set_data('msg_type', msg_type)
123 notification.set_property('icon-name', path_to_image)
124 if 'actions' in pynotify.get_server_caps():
125 notification.add_action('default', 'Default Action',
126 on_pynotify_notification_clicked)
128 try:
129 notification.show()
130 return
131 except gobject.GError, e:
132 # Connection to notification-daemon failed, see #2893
133 gajim.log.debug(str(e))
135 # Either nothing succeeded or the user wants old-style notifications
136 instance = PopupNotificationWindow(event_type, jid, account, msg_type,
137 path_to_image, title, text)
138 gajim.interface.roster.popup_notification_windows.append(instance)
140 def on_pynotify_notification_clicked(notification, action):
141 jid = notification.get_data('jid')
142 account = notification.get_data('account')
143 msg_type = notification.get_data('msg_type')
145 notification.close()
146 gajim.interface.handle_event(account, jid, msg_type)
148 class Notification:
150 Handle notifications
152 def __init__(self):
153 gajim.ged.register_event_handler('notification', ged.GUI2,
154 self._nec_notification)
156 def _nec_notification(self, obj):
157 if obj.do_popup:
158 popup(obj.popup_event_type, obj.jid, obj.conn.name,
159 obj.popup_msg_type, path_to_image=obj.popup_image,
160 title=obj.popup_title, text=obj.popup_text)
162 if obj.do_sound:
163 if obj.sound_file:
164 helpers.play_sound_file(obj.sound_file)
165 elif obj.sound_event:
166 helpers.play_sound(obj.sound_event)
168 if obj.do_command:
169 try:
170 helpers.exec_command(obj.command)
171 except Exception:
172 pass
174 class NotificationResponseManager:
176 Collect references to pending DesktopNotifications and manages there
177 signalling. This is necessary due to a bug in DBus where you can't remove a
178 signal from an interface once it's connected
181 def __init__(self):
182 self.pending = {}
183 self.received = []
184 self.interface = None
186 def attach_to_interface(self):
187 if self.interface is not None:
188 return
189 self.interface = dbus_support.get_notifications_interface()
190 self.interface.connect_to_signal('ActionInvoked',
191 self.on_action_invoked)
192 self.interface.connect_to_signal('NotificationClosed', self.on_closed)
194 def on_action_invoked(self, id_, reason):
195 self.received.append((id_, time.time(), reason))
196 if id_ in self.pending:
197 notification = self.pending[id_]
198 notification.on_action_invoked(id_, reason)
199 del self.pending[id_]
200 if len(self.received) > 20:
201 curt = time.time()
202 for rec in self.received:
203 diff = curt - rec[1]
204 if diff > 10:
205 self.received.remove(rec)
207 def on_closed(self, id_, reason=None):
208 if id_ in self.pending:
209 del self.pending[id_]
211 def add_pending(self, id_, object_):
212 # Check to make sure that we handle an event immediately if we're adding
213 # an id that's already been triggered
214 for rec in self.received:
215 if rec[0] == id_:
216 object_.on_action_invoked(id_, rec[2])
217 self.received.remove(rec)
218 return
219 if id_ not in self.pending:
220 # Add it
221 self.pending[id_] = object_
222 else:
223 # We've triggered an event that has a duplicate ID!
224 gajim.log.debug('Duplicate ID of notification. Can\'t handle this.')
226 notification_response_manager = NotificationResponseManager()
228 class DesktopNotification:
230 A DesktopNotification that interfaces with D-Bus via the Desktop
231 Notification Specification
234 def __init__(self, event_type, jid, account, msg_type='',
235 path_to_image=None, title=None, text=None):
236 self.path_to_image = path_to_image
237 self.event_type = event_type
238 self.title = title
239 self.text = text
240 # 0.3.1 is the only version of notification daemon that has no way
241 # to determine which version it is. If no method exists, it means
242 # they're using that one.
243 self.default_version = [0, 3, 1]
244 self.account = account
245 self.jid = jid
246 self.msg_type = msg_type
248 # default value of text
249 if not text and event_type == 'new_message':
250 # empty text for new_message means do_preview = False
251 self.text = gajim.get_name_from_jid(account, jid)
253 if not title:
254 self.title = event_type # default value
256 if event_type == _('Contact Signed In'):
257 ntype = 'presence.online'
258 elif event_type == _('Contact Signed Out'):
259 ntype = 'presence.offline'
260 elif event_type in (_('New Message'), _('New Single Message'),
261 _('New Private Message')):
262 ntype = 'im.received'
263 elif event_type == _('File Transfer Request'):
264 ntype = 'transfer'
265 elif event_type == _('File Transfer Error'):
266 ntype = 'transfer.error'
267 elif event_type in (_('File Transfer Completed'),
268 _('File Transfer Stopped')):
269 ntype = 'transfer.complete'
270 elif event_type == _('New E-mail'):
271 ntype = 'email.arrived'
272 elif event_type == _('Groupchat Invitation'):
273 ntype = 'im.invitation'
274 elif event_type == _('Contact Changed Status'):
275 ntype = 'presence.status'
276 elif event_type == _('Connection Failed'):
277 ntype = 'connection.failed'
278 elif event_type == _('Subscription request'):
279 ntype = 'subscription.request'
280 elif event_type == _('Unsubscribed'):
281 ntype = 'unsubscribed'
282 else:
283 # default failsafe values
284 self.path_to_image = gtkgui_helpers.get_icon_path(
285 'gajim-chat_msg_recv', 48)
286 ntype = 'im' # Notification Type
288 self.notif = dbus_support.get_notifications_interface(self)
289 if self.notif is None:
290 raise dbus.DBusException('unable to get notifications interface')
291 self.ntype = ntype
293 if self.kde_notifications:
294 self.attempt_notify()
295 else:
296 self.capabilities = self.notif.GetCapabilities()
297 if self.capabilities is None:
298 self.capabilities = ['actions']
299 self.get_version()
301 def attempt_notify(self):
302 timeout = gajim.config.get('notification_timeout') # in seconds
303 ntype = self.ntype
304 if self.kde_notifications:
305 notification_text = ('<html><img src="%(image)s" align=left />' \
306 '%(title)s<br/>%(text)s</html>') % {'title': self.title,
307 'text': self.text, 'image': self.path_to_image}
308 gajim_icon = gtkgui_helpers.get_icon_path('gajim', 48)
309 try:
310 self.notif.Notify(
311 dbus.String(_('Gajim')), # app_name (string)
312 dbus.UInt32(0), # replaces_id (uint)
313 ntype, # event_id (string)
314 dbus.String(gajim_icon), # app_icon (string)
315 dbus.String(''), # summary (string)
316 dbus.String(notification_text), # body (string)
317 # actions (stringlist)
318 (dbus.String('default'), dbus.String(self.event_type),
319 dbus.String('ignore'), dbus.String(_('Ignore'))),
320 [], # hints (not used in KDE yet)
321 dbus.UInt32(timeout*1000), # timeout (int), in ms
322 reply_handler=self.attach_by_id,
323 error_handler=self.notify_another_way)
324 return
325 except Exception:
326 pass
327 version = self.version
328 if version[:2] == [0, 2]:
329 actions = {}
330 if 'actions' in self.capabilities:
331 actions = {'default': 0}
332 try:
333 self.notif.Notify(
334 dbus.String(_('Gajim')),
335 dbus.String(self.path_to_image),
336 dbus.UInt32(0),
337 ntype,
338 dbus.Byte(0),
339 dbus.String(self.title),
340 dbus.String(self.text),
341 [dbus.String(self.path_to_image)],
342 actions,
343 [''],
344 True,
345 dbus.UInt32(timeout),
346 reply_handler=self.attach_by_id,
347 error_handler=self.notify_another_way)
348 except AttributeError:
349 # we're actually dealing with the newer version
350 version = [0, 3, 1]
351 if version > [0, 3]:
352 if gajim.interface.systray_enabled and \
353 gajim.config.get('attach_notifications_to_systray'):
354 status_icon = gajim.interface.systray.status_icon
355 x, y, width, height = status_icon.get_geometry()[1]
356 pos_x = x + (width / 2)
357 pos_y = y + (height / 2)
358 hints = {'x': pos_x, 'y': pos_y}
359 else:
360 hints = {}
361 if version >= [0, 3, 2]:
362 hints['urgency'] = dbus.Byte(0) # Low Urgency
363 hints['category'] = dbus.String(ntype)
364 # it seems notification-daemon doesn't like empty text
365 if self.text:
366 text = self.text
367 else:
368 text = ' '
369 if os.environ.get('KDE_FULL_SESSION') == 'true':
370 self.path_to_image = os.path.abspath(self.path_to_image)
371 text = '<table style=\'padding: 3px\'><tr><td>' \
372 '<img src=\"%s\"></td><td width=20> </td>' \
373 '<td>%s</td></tr></table>' % (self.path_to_image,
374 text)
375 self.path_to_image = os.path.abspath(
376 gtkgui_helpers.get_icon_path('gajim', 48))
377 actions = ()
378 if 'actions' in self.capabilities:
379 actions = (dbus.String('default'), dbus.String(
380 self.event_type))
381 try:
382 self.notif.Notify(
383 dbus.String(_('Gajim')),
384 # this notification does not replace other
385 dbus.UInt32(0),
386 dbus.String(self.path_to_image),
387 dbus.String(self.title),
388 dbus.String(text),
389 actions,
390 hints,
391 dbus.UInt32(timeout*1000),
392 reply_handler=self.attach_by_id,
393 error_handler=self.notify_another_way)
394 except Exception, e:
395 self.notify_another_way(e)
396 else:
397 try:
398 self.notif.Notify(
399 dbus.String(_('Gajim')),
400 dbus.String(self.path_to_image),
401 dbus.UInt32(0),
402 dbus.String(self.title),
403 dbus.String(self.text),
404 dbus.String(''),
405 hints,
406 dbus.UInt32(timeout*1000),
407 reply_handler=self.attach_by_id,
408 error_handler=self.notify_another_way)
409 except Exception, e:
410 self.notify_another_way(e)
412 def attach_by_id(self, id_):
413 self.id = id_
414 notification_response_manager.attach_to_interface()
415 notification_response_manager.add_pending(self.id, self)
417 def notify_another_way(self, e):
418 gajim.log.debug('Error when trying to use notification daemon: %s' % \
419 str(e))
420 instance = PopupNotificationWindow(self.event_type, self.jid,
421 self.account, self.msg_type, self.path_to_image, self.title,
422 self.text)
423 gajim.interface.roster.popup_notification_windows.append(instance)
425 def on_action_invoked(self, id_, reason):
426 if self.notif is None:
427 return
428 self.notif.CloseNotification(dbus.UInt32(id_))
429 self.notif = None
431 if reason == 'ignore':
432 return
434 gajim.interface.handle_event(self.account, self.jid, self.msg_type)
436 def version_reply_handler(self, name, vendor, version, spec_version=None):
437 if spec_version:
438 version = spec_version
439 elif vendor == 'Xfce' and version.startswith('0.1.0'):
440 version = '0.9'
441 version_list = version.split('.')
442 self.version = []
443 try:
444 while len(version_list):
445 self.version.append(int(version_list.pop(0)))
446 except ValueError:
447 self.version_error_handler_3_x_try(None)
448 self.attempt_notify()
450 def get_version(self):
451 self.notif.GetServerInfo(
452 reply_handler=self.version_reply_handler,
453 error_handler=self.version_error_handler_2_x_try)
455 def version_error_handler_2_x_try(self, e):
456 self.notif.GetServerInformation(
457 reply_handler=self.version_reply_handler,
458 error_handler=self.version_error_handler_3_x_try)
460 def version_error_handler_3_x_try(self, e):
461 self.version = self.default_version
462 self.attempt_notify()