From 6e413e7f5a3645943d10d0c75fab06e7c301755e Mon Sep 17 00:00:00 2001 From: Nikos Kouremenos Date: Sat, 10 Dec 2005 00:56:38 +0000 Subject: [PATCH] [lorien420] Notification Daemon Popups are now clickable! CONGRAAAAAAATS Andrew Sayman --- src/dbus_support.py | 117 ++++++++++++++++ src/notify.py | 317 +++++++++++++++++++++++++----------------- src/remote_control.py | 55 +++----- 3 files changed, 325 insertions(+), 164 deletions(-) create mode 100644 src/dbus_support.py diff --git a/src/dbus_support.py b/src/dbus_support.py new file mode 100644 index 000000000..6450df7b6 --- /dev/null +++ b/src/dbus_support.py @@ -0,0 +1,117 @@ +## dbus_support.py +## +## Contributors for this file: +## - Andrew Sayman +## +## Copyright (C) 2003-2004 Yann Le Boulanger +## Vincent Hanquez +## Copyright (C) 2005 Yann Le Boulanger +## Vincent Hanquez +## Nikos Kouremenos +## Dimitur Kirov +## Travis Shirk +## Norman Rasmussen +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +import os +import sys + +from common import exceptions +from common import i18n +_ = i18n._ + +try: + import dbus + version = getattr(dbus, 'version', (0, 20, 0)) +except ImportError: + version = (0, 0, 0) + +if version >= (0, 41, 0): + import dbus.service + import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it + +supported = True +if 'dbus' not in globals() and not os.name == 'nt': + print _('D-Bus python bindings are missing in this computer') + print _('D-Bus capabilities of Gajim cannot be used') + supported = False +# dbus 0.23 leads to segfault with threads_init() +if sys.version[:4] >= '2.4' and version[1] < 30: + supported = False + +class SessionBus: + '''A Singleton for the DBus SessionBus''' + def __init__(self): + self.session_bus = None + + def SessionBus(self): + if not supported: + raise exceptions.DbusNotSupported + + if not self.present(): + raise exceptions.SessionBusNotPresent + return self.session_bus + + def bus(self): + return self.SessionBus() + + def present(self): + if not supported: + return False + if self.session_bus is None: + try: + self.session_bus = dbus.SessionBus() + except dbus.DBusException: + self.session_bus = None + return False + if self.session_bus is None: + return False + return True + +session_bus = SessionBus() + +def get_interface(interface, path): + '''Returns an interface on the current SessionBus. If the interface isn't + running, it tries to start it first.''' + if not supported: + return None + if session_bus.present(): + bus = session_bus.SessionBus() + else: + return None + try: + obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + dbus_iface = dbus.Interface(obj, 'org.freedesktop.DBus') + running_services = dbus_iface.ListNames() + started = True + if interface not in running_services: + # try to start the service + if dbus_iface.StartServiceByName(interface, dbus.UInt32(0)) == 1: + started = True + else: + started = False + if not started: + return None + obj = bus.get_object(interface, path) + return dbus.Interface(obj, interface) + except Exception, e: + print >> sys.stderr, e + return None + except dbus.DBusException, e: + # This exception could give useful info about why notification breaks + print >> sys.stderr, e + return None + + +def get_notifications_interface(): + '''Returns the notifications interface.''' + return get_interface('org.freedesktop.Notifications','/org/freedesktop/Notifications') diff --git a/src/notify.py b/src/notify.py index 6aceb24c5..9c806774c 100644 --- a/src/notify.py +++ b/src/notify.py @@ -28,152 +28,36 @@ ## GNU General Public License for more details. ## -HAS_DBUS = True - -try: - import dbus -except ImportError: - HAS_DBUS = False - import os import sys import gajim import dialogs +import gobject from common import gajim +from common import exceptions from common import i18n i18n.init() _ = i18n._ - -def dbus_get_interface(): - try: - interface = 'org.freedesktop.Notifications' - path = '/org/freedesktop/Notifications' - bus = dbus.SessionBus() - obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') - dbus_iface = dbus.Interface(obj, 'org.freedesktop.DBus') - running_services = dbus_iface.ListNames() - started = True - if interface not in running_services: - # try to start the service (notif-daemon) - if dbus_iface.StartServiceByName(interface, dbus.UInt32(0)) == 1: - started = True - else: - started = False - if not started: - return None - obj = bus.get_object(interface, path) - return dbus.Interface(obj, interface) - except Exception, e: - return None - except dbus.DBusException, e: - # This exception could give useful info about why notification breaks - print >> sys.stderr, e - return None - -def dbus_available(): - if not HAS_DBUS: - return False - if dbus_get_interface() is None: - return False - else: - return True - -def dbus_notify(event_type, jid, account, msg_type = '', file_props = None): - if jid in gajim.contacts[account]: - actor = gajim.get_first_contact_instance_from_jid(account, jid).name - else: - actor = jid - - # default value of txt - txt = actor - - img = 'chat.png' # img to display - ntype = 'im' # Notification Type - - if event_type == _('Contact Signed In'): - img = 'online.png' - ntype = 'presence.online' - elif event_type == _('Contact Signed Out'): - img = 'offline.png' - ntype = 'presence.offline' - elif event_type in (_('New Message'), _('New Single Message'), - _('New Private Message')): - img = 'chat.png' # FIXME: better img and split events - ntype = 'im.received' - if event_type == _('New Private Message'): - room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - room_name,t = gajim.get_room_name_and_server_from_room_jid(room_jid) - txt = _('%(nickname)s in room %(room_name)s has sent you a new message.')\ - % {'nickname': nick, 'room_name': room_name} - else: - #we talk about a name here - txt = _('%s has sent you a new message.') % actor - elif event_type == _('File Transfer Request'): - img = 'requested.png' # FIXME: better img - ntype = 'transfer' - #we talk about a name here - txt = _('%s wants to send you a file.') % actor - elif event_type == _('File Transfer Error'): - img = 'error.png' # FIXME: better img - ntype = 'transfer.error' - elif event_type in (_('File Transfer Completed'), _('File Transfer Stopped')): - img = 'closed.png' # # FIXME: better img and split events - ntype = 'transfer.complete' - if file_props is not None: - if file_props['type'] == 'r': - # get the name of the sender, as it is in the roster - sender = unicode(file_props['sender']).split('/')[0] - name = gajim.get_first_contact_instance_from_jid( - account, sender).name - filename = os.path.basename(file_props['file-name']) - if event_type == _('File Transfer Completed'): - txt = _('You successfully received %(filename)s from %(name)s.')\ - % {'filename': filename, 'name': name} - else: # ft stopped - txt = _('File transfer of %(filename)s from %(name)s stopped.')\ - % {'filename': filename, 'name': name} - else: - receiver = file_props['receiver'] - if hasattr(receiver, 'jid'): - receiver = receiver.jid - receiver = receiver.split('/')[0] - # get the name of the contact, as it is in the roster - name = gajim.get_first_contact_instance_from_jid( - account, receiver).name - filename = os.path.basename(file_props['file-name']) - if event_type == _('File Transfer Completed'): - txt = _('You successfully sent %(filename)s to %(name)s.')\ - % {'filename': filename, 'name': name} - else: # ft stopped - txt = _('File transfer of %(filename)s to %(name)s stopped.')\ - % {'filename': filename, 'name': name} - else: - txt = '' - - iconset = gajim.config.get('iconset') - if not iconset: - iconset = 'sun' - # FIXME: use 32x32 or 48x48 someday - path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16', img) - path = os.path.abspath(path) - notif = dbus_get_interface() - if notif is None: - raise dbus.DBusException() - notif.Notify(dbus.String(_('Gajim')), - dbus.String(path), dbus.UInt32(0), ntype, dbus.Byte(0), - dbus.String(event_type), dbus.String(txt), - [dbus.String(path)], [''], [''], True, dbus.UInt32(5)) +import dbus_support +if dbus_support.supported: + import dbus + if dbus_support.version >= (0, 41, 0): + import dbus.glib + import dbus.service def notify(event_type, jid, account, msg_type = '', file_props = None): - if dbus_available() and gajim.config.get('use_notif_daemon'): + '''Notifies a user of an event. It first tries to a valid implementation of + the Desktop Notification Specification. If that fails, then we fall back to + the older style PopupNotificationWindow method.''' + if gajim.config.get('use_notif_daemon') and dbus_support.supported: try: - dbus_notify(event_type, jid, account, msg_type, file_props) + DesktopNotification(event_type, jid, account, msg_type, file_props) return except dbus.DBusException, e: # Connection to DBus failed, try popup - pass + print >> sys.stderr, e except TypeError, e: # This means that we sent the message incorrectly print >> sys.stderr, e @@ -181,4 +65,177 @@ def notify(event_type, jid, account, msg_type = '', file_props = None): msg_type, file_props) gajim.interface.roster.popup_notification_windows.append(instance) +class NotificationResponseManager: + '''Collects references to pending DesktopNotifications and manages there + signalling. This is necessary due to a bug in DBus where you can't remove + a signal from an interface once it's connected.''' + def __init__(self): + self.pending = {} + self.interface = None + def attach_to_interface(self): + if self.interface is not None: + return + self.interface = dbus_support.get_notifications_interface() + self.interface.connect_to_signal('ActionInvoked', self.on_action_invoked) + self.interface.connect_to_signal('NotificationClosed', self.on_closed) + + def on_action_invoked(self, id, reason): + if self.pending.has_key(id): + notification = self.pending[id] + notification.on_action_invoked(id, reason) + del self.pending[id] + else: + # This happens in the case of a race condition where the user clicks + # on a popup before the program finishes registering this callback + gobject.timeout_add(1000, self.on_action_invoked, id, reason) + + def on_closed(self, id, reason): + if self.pending.has_key(id): + del self.pending[id] + +notification_response_manager = NotificationResponseManager() + +class DesktopNotification: + '''A DesktopNotification that interfaces with DBus via the Desktop + Notification specification''' + def __init__(self, event_type, jid, account, msg_type = '', file_props = None): + self.account = account + self.jid = jid + self.msg_type = msg_type + self.file_props = file_props + + if jid in gajim.contacts[account]: + actor = gajim.get_first_contact_instance_from_jid(account, jid).name + else: + actor = jid + + # default value of txt + txt = actor + + img = 'chat.png' # img to display + ntype = 'im' # Notification Type + + if event_type == _('Contact Signed In'): + img = 'online.png' + ntype = 'presence.online' + elif event_type == _('Contact Signed Out'): + img = 'offline.png' + ntype = 'presence.offline' + elif event_type in (_('New Message'), _('New Single Message'), + _('New Private Message')): + img = 'chat.png' # FIXME: better img and split events + ntype = 'im.received' + if event_type == _('New Private Message'): + room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) + room_name,t = gajim.get_room_name_and_server_from_room_jid(room_jid) + txt = _('%(nickname)s in room %(room_name)s has sent you a new message.')\ + % {'nickname': nick, 'room_name': room_name} + else: + #we talk about a name here + txt = _('%s has sent you a new message.') % actor + elif event_type == _('File Transfer Request'): + img = 'requested.png' # FIXME: better img + ntype = 'transfer' + #we talk about a name here + txt = _('%s wants to send you a file.') % actor + elif event_type == _('File Transfer Error'): + img = 'error.png' # FIXME: better img + ntype = 'transfer.error' + elif event_type in (_('File Transfer Completed'), _('File Transfer Stopped')): + img = 'closed.png' # # FIXME: better img and split events + ntype = 'transfer.complete' + if file_props is not None: + if file_props['type'] == 'r': + # get the name of the sender, as it is in the roster + sender = unicode(file_props['sender']).split('/')[0] + name = gajim.get_first_contact_instance_from_jid( + account, sender).name + filename = os.path.basename(file_props['file-name']) + if event_type == _('File Transfer Completed'): + txt = _('You successfully received %(filename)s from %(name)s.')\ + % {'filename': filename, 'name': name} + else: # ft stopped + txt = _('File transfer of %(filename)s from %(name)s stopped.')\ + % {'filename': filename, 'name': name} + else: + receiver = file_props['receiver'] + if hasattr(receiver, 'jid'): + receiver = receiver.jid + receiver = receiver.split('/')[0] + # get the name of the contact, as it is in the roster + name = gajim.get_first_contact_instance_from_jid( + account, receiver).name + filename = os.path.basename(file_props['file-name']) + if event_type == _('File Transfer Completed'): + txt = _('You successfully sent %(filename)s to %(name)s.')\ + % {'filename': filename, 'name': name} + else: # ft stopped + txt = _('File transfer of %(filename)s to %(name)s stopped.')\ + % {'filename': filename, 'name': name} + else: + txt = '' + + iconset = gajim.config.get('iconset') + if not iconset: + iconset = 'sun' + # FIXME: use 32x32 or 48x48 someday + path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16', img) + path = os.path.abspath(path) + self.notif = dbus_support.get_notifications_interface() + if self.notif is None: + raise dbus.DBusException() + self.id = self.notif.Notify(dbus.String(_('Gajim')), + dbus.String(path), dbus.UInt32(0), ntype, dbus.Byte(0), + dbus.String(event_type), dbus.String(txt), + [dbus.String(path)], {'default':0}, [''], True, dbus.UInt32(5)) + notification_response_manager.attach_to_interface() + notification_response_manager.pending[self.id] = self + + def on_action_invoked(self, id, reason): + if self.notif is None: + return + self.notif.CloseNotification(dbus.UInt32(id)) + self.notif = None + # use Contact class, new_chat expects it that way + # is it in the roster? + if gajim.contacts[self.account].has_key(self.jid): + contact = gajim.get_contact_instance_with_highest_priority( + self.account, self.jid) + else: + from gajim import Contact + keyID = '' + attached_keys = gajim.config.get_per('accounts', self.account, + 'attached_gpg_keys').split() + if self.jid in attached_keys: + keyID = attached_keys[attached_keys.index(jid) + 1] + if self.msg_type.find('file') != 0: + if self.msg_type == 'pm': + room_jid, nick = self.jid.split('/', 1) + show = gajim.gc_contacts[self.account][room_jid][nick].show + contact = Contact(jid = self.jid, name = nick, groups = ['none'], + show = show, sub = 'none') + else: + contact = Contact(jid = self.jid, name = self.jid.split('@')[0], + groups = [_('not in the roster')], show = 'not in the roster', + status = _('not in the roster'), sub = 'none', keyID = keyID) + gajim.contacts[self.account][self.jid] = [contact] + gajim.interface.roster.add_contact_to_roster(contact.jid, + self.account) + + if self.msg_type == 'pm': # It's a private message + gajim.interface.roster.new_chat(contact, self.account) + chats_window = gajim.interface.instances[self.account]['chats'][self.jid] + chats_window.set_active_tab(self.jid) + chats_window.window.present() + elif self.msg_type in ('normal', 'file-request', 'file-request-error', + 'file-send-error', 'file-error', 'file-stopped', 'file-completed'): + # Get the first single message event + ev = gajim.get_first_event(self.account, self.jid, self.msg_type) + gajim.interface.roster.open_event(self.account, self.jid, ev) + + else: # 'chat' + gajim.interface.roster.new_chat(contact, self.account) + chats_window = gajim.interface.instances[self.account]['chats'][self.jid] + chats_window.set_active_tab(self.jid) + chats_window.window.present() diff --git a/src/remote_control.py b/src/remote_control.py index 9fd7d1649..1287a636b 100644 --- a/src/remote_control.py +++ b/src/remote_control.py @@ -4,6 +4,7 @@ ## - Yann Le Boulanger ## - Nikos Kouremenos ## - Dimitur Kirov +## - Andrew Sayman ## ## Copyright (C) 2003-2004 Yann Le Boulanger ## Vincent Hanquez @@ -38,20 +39,17 @@ from common import i18n from dialogs import AddNewContactWindow _ = i18n._ -try: +import dbus_support +if dbus_support.supported: import dbus - _version = getattr(dbus, 'version', (0, 20, 0)) -except ImportError: - _version = (0, 0, 0) - -if _version >= (0, 41, 0): - import dbus.service - import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it - DbusPrototype = dbus.service.Object -elif _version >= (0, 20, 0): - DbusPrototype = dbus.Object -else: #dbus is not defined - DbusPrototype = str + if dbus_support.version >= (0, 41, 0): + import dbus.service + import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it + DbusPrototype = dbus.service.Object + elif dbus_support.version >= (0, 20, 0): + DbusPrototype = dbus.Object + else: #dbus is not defined + DbusPrototype = str INTERFACE = 'org.gajim.dbus.RemoteInterface' OBJ_PATH = '/org/gajim/dbus/RemoteObject' @@ -63,23 +61,12 @@ STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', class Remote: def __init__(self): self.signal_object = None - if 'dbus' not in globals() and not os.name == 'nt': - print _('D-Bus python bindings are missing in this computer') - print _('D-Bus capabilities of Gajim cannot be used') - raise exceptions.DbusNotSupported - # dbus 0.23 leads to segfault with threads_init() - if sys.version[:4] >= '2.4' and _version[1] < 30: - raise exceptions.DbusNotSupported - - try: - session_bus = dbus.SessionBus() - except: - raise exceptions.SessionBusNotPresent + session_bus = dbus_support.session_bus.SessionBus() - if _version[1] >= 41: + if dbus_support.version[1] >= 41: service = dbus.service.BusName(SERVICE, bus=session_bus) self.signal_object = SignalObject(service) - elif _version[1] <= 40 and _version[1] >= 20: + elif dbus_support.version[1] <= 40 and dbus_support.version[1] >= 20: service=dbus.Service(SERVICE, session_bus) self.signal_object = SignalObject(service) @@ -97,9 +84,9 @@ class SignalObject(DbusPrototype): self.vcard_account = None # register our dbus API - if _version[1] >= 41: + if dbus_support.version[1] >= 41: DbusPrototype.__init__(self, service, OBJ_PATH) - elif _version[1] >= 30: + elif dbus_support.version[1] >= 30: DbusPrototype.__init__(self, OBJ_PATH, service) else: DbusPrototype.__init__(self, OBJ_PATH, service, @@ -123,7 +110,7 @@ class SignalObject(DbusPrototype): def raise_signal(self, signal, arg): ''' raise a signal, with a single string message ''' - if _version[1] >= 30: + if dbus_support.version[1] >= 30: from dbus import dbus_bindings message = dbus_bindings.Signal(OBJ_PATH, INTERFACE, signal) i = message.get_iter(True) @@ -437,7 +424,7 @@ class SignalObject(DbusPrototype): def _get_real_arguments(self, args, desired_length): # supresses the first 'message' argument, which is set in dbus 0.23 - if _version[1] == 20: + if dbus_support.version[1] == 20: args=args[1:] if desired_length > 0: args = list(args) @@ -473,14 +460,14 @@ class SignalObject(DbusPrototype): return repr(contact_dict) - if _version[1] >= 30 and _version[1] <= 40: + if dbus_support.version[1] >= 30 and dbus_support.version[1] <= 40: method = dbus.method signal = dbus.signal - elif _version[1] >= 41: + elif dbus_support.version[1] >= 41: method = dbus.service.method signal = dbus.service.signal - if _version[1] >= 30: + if dbus_support.version[1] >= 30: # prevent using decorators, because they are not supported # on python < 2.4 # FIXME: use decorators when python2.3 (and dbus 0.23) is OOOOOOLD