Skip to content
Snippets Groups Projects
Commit f4f8db52 authored by Yann Leboulanger's avatar Yann Leboulanger
Browse files

Merge branch 'gnotification' into 'master'

Use GNotification instead of pynotify or dbus

See merge request !130
parents 28184273 66e9bc4e
No related branches found
No related tags found
No related merge requests found
......@@ -266,3 +266,9 @@ class AppActions():
else:
interface.instances['logs'] = history_window.\
HistoryWindow()
def on_open_event(self, action, param):
dict_ = param.unpack()
app.interface.handle_event(dict_['account'], dict_['jid'],
dict_['type_'])
......@@ -74,7 +74,6 @@ class Config:
'autopopupaway': [ opt_bool, False ],
'autopopup_chat_opened': [ opt_bool, False, _('Show desktop notification even when a chat window is opened for this contact and does not have focus') ],
'sounddnd': [ opt_bool, False, _('Play sound when user is busy')],
'use_notif_daemon': [ opt_bool, True, _('Use D-Bus and Notification-Daemon to show notifications') ],
'showoffline': [ opt_bool, False ],
'show_only_chat_and_online': [ opt_bool, False, _('Show only online and free for chat contacts in roster.')],
'show_transports_group': [ opt_bool, True ],
......
......@@ -159,27 +159,6 @@ def get_interface(interface, path, start_service=True):
return None
def get_notifications_interface(notif=None):
"""
Get the notifications interface
:param notif: DesktopNotification instance
"""
# try to see if KDE notifications are available
iface = get_interface('org.kde.VisualNotifications', '/VisualNotifications',
start_service=False)
if iface != None:
if notif != None:
notif.kde_notifications = True
return iface
# KDE notifications don't seem to be available, falling back to
# notification-daemon
else:
if notif != None:
notif.kde_notifications = False
return get_interface('org.freedesktop.Notifications',
'/org/freedesktop/Notifications')
if supported:
class MissingArgument(dbus.DBusException):
_dbus_error_name = _GAJIM_ERROR_IFACE + '.MissingArgument'
......
......@@ -73,10 +73,6 @@ class FeaturesWindow:
_('Spellchecking of composed messages.'),
_('Requires libgtkspell.'),
_('Requires libgtkspell and libenchant.')),
_('Notification'): (self.notification_available,
_('Passive popups notifying for new events.'),
_('Requires python-notify or instead python-dbus in conjunction with notification-daemon.'),
_('Feature not available under Windows.')),
_('Automatic status'): (self.idle_available,
_('Ability to measure idle time, in order to set auto status.'),
_('Requires libxss library.'),
......@@ -199,18 +195,6 @@ class FeaturesWindow:
return False
return True
def notification_available(self):
if os.name == 'nt':
return False
from gajim.common import dbus_support
if self.dbus_available() and dbus_support.get_notifications_interface():
return True
try:
__import__('pynotify')
except Exception:
return False
return True
def idle_available(self):
from gajim.common import sleepy
return sleepy.SUPPORTED
......
......@@ -314,7 +314,8 @@ class GajimApplication(Gtk.Application):
('-delete-motd', action.on_delete_motd, 'online', 's'),
('-activate-bookmark',
action.on_activate_bookmark, 'online', 'a{sv}'),
('-import-contacts', action.on_import_contacts, 'online', 's')
('-open-event', action.on_open_event, 'always', 'a{sv}'),
('-import-contacts', action.on_import_contacts, 'online', 's'),
]
# General Stateful Actions
......@@ -347,7 +348,7 @@ class GajimApplication(Gtk.Application):
('features', action.on_features),
('content', action.on_contents),
('about', action.on_about),
('faq', action.on_faq)
('faq', action.on_faq),
]
for action in self.general_actions:
......
......@@ -27,31 +27,16 @@
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
import os
import time
import sys
from gajim.dialogs import PopupNotificationWindow
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from gajim import gtkgui_helpers
from gajim.common import app
from gajim.common import helpers
from gajim.common import ged
from gajim.common import dbus_support
if dbus_support.supported:
import dbus
USER_HAS_PYNOTIFY = True # user has pynotify module
try:
import gi
gi.require_version('Notify', '0.7')
from gi.repository import Notify
Notify.init('Gajim Notification')
except ValueError:
USER_HAS_PYNOTIFY = False
def get_show_in_roster(event, account, contact, session=None):
"""
Return True if this event must be shown in roster, else False
......@@ -74,12 +59,11 @@ def get_show_in_systray(event, account, contact, type_=None):
return False
return app.config.get('trayicon_notification_on_events')
def popup(event_type, jid, account, msg_type='', path_to_image=None, title=None,
def popup(event_type, jid, account, type_='', path_to_image=None, title=None,
text=None, timeout=-1):
"""
Notify 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
Notify a user of an event using GNotification and GApplication under linux,
the older style PopupNotificationWindow method under windows
"""
# default image
if not path_to_image:
......@@ -88,67 +72,44 @@ text=None, timeout=-1):
if timeout < 0:
timeout = app.config.get('notification_timeout')
# Try to show our popup via D-Bus and notification daemon
if app.config.get('use_notif_daemon') and dbus_support.supported:
try:
DesktopNotification(event_type, jid, account, msg_type,
path_to_image, title, GLib.markup_escape_text(text), timeout)
return # sucessfully did D-Bus Notification procedure!
except dbus.DBusException as e:
# Connection to D-Bus failed
app.log.debug(str(e))
except TypeError as e:
# This means that we sent the message incorrectly
app.log.debug(str(e))
# Ok, that failed. Let's try pynotify, which also uses notification daemon
if app.config.get('use_notif_daemon') and USER_HAS_PYNOTIFY:
if not text and event_type == 'new_message':
# empty text for new_message means do_preview = False
# -> default value for text
_text = GLib.markup_escape_text(app.get_name_from_jid(account,
jid))
else:
_text = GLib.markup_escape_text(text)
if not title:
_title = ''
else:
_title = title
notification = Notify.Notification.new(_title, _text)
notification.set_timeout(timeout*1000)
notification.set_category(event_type)
notification._data = {}
notification._data["event_type"] = event_type
notification._data["jid"] = jid
notification._data["account"] = account
notification._data["msg_type"] = msg_type
notification.set_property('icon-name', path_to_image)
if 'actions' in Notify.get_server_caps():
notification.add_action('default', 'Default Action',
on_pynotify_notification_clicked)
try:
notification.show()
return
except GObject.GError as e:
# Connection to notification-daemon failed, see #2893
app.log.debug(str(e))
# Either nothing succeeded or the user wants old-style notifications
instance = PopupNotificationWindow(event_type, jid, account, msg_type,
path_to_image, title, text, timeout)
app.interface.roster.popup_notification_windows.append(instance)
def on_pynotify_notification_clicked(notification, action):
jid = notification._data.jid
account = notification._data.account
msg_type = notification._data.msg_type
if sys.platform == 'win32':
instance = PopupNotificationWindow(event_type, jid, account, type_,
path_to_image, title, text, timeout)
app.interface.roster.popup_notification_windows.append(instance)
return
# use GNotification
# TODO: Move to standard GTK+ icons here.
icon = Gio.FileIcon.new(Gio.File.new_for_path(path_to_image))
notification = Gio.Notification()
if title is not None:
notification.set_title(title)
if text is not None:
notification.set_body(text)
notification.set_icon(icon)
notif_id = None
if event_type in (_('Contact Signed In'), _('Contact Signed Out'),
_('New Message'), _('New Single Message'), _('New Private Message'),
_('Contact Changed Status'), _('File Transfer Request'),
_('File Transfer Error'), _('File Transfer Completed'),
_('File Transfer Stopped'), _('Groupchat Invitation'),
_('Connection Failed'), _('Subscription request'), _('Unsubscribed')):
# Create Variant Dict
dict_ = {'account': GLib.Variant('s', account),
'jid': GLib.Variant('s', jid),
'type_': GLib.Variant('s', type_)}
variant_dict = GLib.Variant('a{sv}', dict_)
action = 'app.{}-open-event'.format(account)
notification.add_button_with_target('Open', action, variant_dict)
notification.set_default_action_and_target(action, variant_dict)
if event_type in (_('New Message'), _('New Single Message'),
_('New Private Message')):
# Only one notification per JID
notif_id = jid
notification.set_priority(Gio.NotificationPriority.NORMAL)
notification.set_urgent(False)
app.app.send_notification(notif_id, notification)
notification.close()
app.interface.handle_event(account, jid, msg_type)
class Notification:
"""
......@@ -184,297 +145,3 @@ class Notification:
helpers.exec_command(obj.command, use_shell=True)
except Exception:
pass
class NotificationResponseManager:
"""
Collect 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.received = []
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 id_ in self.pending:
notification = self.pending[id_]
notification.on_action_invoked(id_, reason)
del self.pending[id_]
return
# got an action on popup that isn't handled yet? Maybe user clicked too
# fast. Remember it.
self.received.append((id_, time.time(), reason))
if len(self.received) > 20:
curt = time.time()
for rec in self.received:
diff = curt - rec[1]
if diff > 10:
self.received.remove(rec)
def on_closed(self, id_, reason=None):
if id_ in self.pending:
del self.pending[id_]
def add_pending(self, id_, object_):
# Check to make sure that we handle an event immediately if we're adding
# an id that's already been triggered
for rec in self.received:
if rec[0] == id_:
object_.on_action_invoked(id_, rec[2])
self.received.remove(rec)
return
if id_ not in self.pending:
# Add it
self.pending[id_] = object_
else:
# We've triggered an event that has a duplicate ID!
app.log.debug('Duplicate ID of notification. Can\'t handle this.')
notification_response_manager = NotificationResponseManager()
class DesktopNotification:
"""
A DesktopNotification that interfaces with D-Bus via the Desktop
Notification Specification
"""
def __init__(self, event_type, jid, account, msg_type='',
path_to_image=None, title=None, text=None, timeout=-1):
self.path_to_image = os.path.abspath(path_to_image)
self.event_type = event_type
self.title = title
self.text = text
self.timeout = timeout
# 0.3.1 is the only version of notification daemon that has no way
# to determine which version it is. If no method exists, it means
# they're using that one.
self.default_version = [0, 3, 1]
self.account = account
self.jid = jid
self.msg_type = msg_type
# default value of text
if not text and event_type == 'new_message':
# empty text for new_message means do_preview = False
self.text = app.get_name_from_jid(account, jid)
if not title:
self.title = event_type # default value
if event_type == _('Contact Signed In'):
ntype = 'presence.online'
elif event_type == _('Contact Signed Out'):
ntype = 'presence.offline'
elif event_type in (_('New Message'), _('New Single Message'),
_('New Private Message')):
ntype = 'im.received'
elif event_type == _('File Transfer Request'):
ntype = 'transfer'
elif event_type == _('File Transfer Error'):
ntype = 'transfer.error'
elif event_type in (_('File Transfer Completed'),
_('File Transfer Stopped')):
ntype = 'transfer.complete'
elif event_type == _('New E-mail'):
ntype = 'email.arrived'
elif event_type == _('Groupchat Invitation'):
ntype = 'im.invitation'
elif event_type == _('Contact Changed Status'):
ntype = 'presence.status'
elif event_type == _('Connection Failed'):
ntype = 'connection.failed'
elif event_type == _('Subscription request'):
ntype = 'subscription.request'
elif event_type == _('Unsubscribed'):
ntype = 'unsubscribed'
else:
# default failsafe values
self.path_to_image = gtkgui_helpers.get_icon_path(
'gajim-chat_msg_recv', 48)
ntype = 'im' # Notification Type
self.notif = dbus_support.get_notifications_interface(self)
if self.notif is None:
raise dbus.DBusException('unable to get notifications interface')
self.ntype = ntype
if self.kde_notifications:
self.attempt_notify()
else:
self.capabilities = self.notif.GetCapabilities()
if self.capabilities is None:
self.capabilities = ['actions']
self.get_version()
def attempt_notify(self):
ntype = self.ntype
if self.kde_notifications:
notification_text = ('<html><img src="%(image)s" align=left />' \
'%(title)s<br/>%(text)s</html>') % {'title': self.title,
'text': self.text, 'image': self.path_to_image}
gajim_icon = gtkgui_helpers.get_icon_path('org.gajim.Gajim', 48)
try:
self.notif.Notify(
dbus.String(_('Gajim')), # app_name (string)
dbus.UInt32(0), # replaces_id (uint)
ntype, # event_id (string)
dbus.String(gajim_icon), # app_icon (string)
dbus.String(''), # summary (string)
dbus.String(notification_text), # body (string)
# actions (stringlist)
(dbus.String('default'), dbus.String(self.event_type),
dbus.String('ignore'), dbus.String(_('Ignore'))),
[], # hints (not used in KDE yet)
dbus.UInt32(self.timeout*1000), # timeout (int), in ms
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
return
except Exception:
pass
version = self.version
if version[:2] == [0, 2]:
actions = {}
if 'actions' in self.capabilities and self.msg_type:
actions = {'default': 0}
try:
self.notif.Notify(
dbus.String(_('Gajim')),
dbus.String(self.path_to_image),
dbus.UInt32(0),
ntype,
dbus.Byte(0),
dbus.String(self.title),
dbus.String(self.text),
[dbus.String(self.path_to_image)],
actions,
[''],
True,
dbus.UInt32(self.timeout),
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
except AttributeError:
# we're actually dealing with the newer version
version = [0, 3, 1]
if version > [0, 3]:
if app.interface.systray_enabled and \
app.config.get('attach_notifications_to_systray'):
status_icon = app.interface.systray.status_icon
rect = status_icon.get_geometry()[2]
x, y, width, height = rect.x, rect.y, rect.width, rect.height
pos_x = x + (width / 2)
pos_y = y + (height / 2)
hints = {'x': pos_x, 'y': pos_y}
else:
hints = {}
if version >= [0, 3, 2]:
hints['urgency'] = dbus.Byte(0) # Low Urgency
hints['category'] = dbus.String(ntype)
# it seems notification-daemon doesn't like empty text
if self.text:
text = self.text
if len(self.text) > 200:
text = '%s\n' % self.text[:200]
else:
text = ' '
if os.environ.get('KDE_FULL_SESSION') == 'true':
text = '<table style=\'padding: 3px\'><tr><td>' \
'<img src=\"%s\"></td><td width=20> </td>' \
'<td>%s</td></tr></table>' % (self.path_to_image,
text)
self.path_to_image = os.path.abspath(
gtkgui_helpers.get_icon_path('org.gajim.Gajim', 48))
actions = ()
if 'actions' in self.capabilities and self.msg_type:
actions = (dbus.String('default'), dbus.String(
self.event_type))
try:
self.notif.Notify(
dbus.String(_('Gajim')),
# this notification does not replace other
dbus.UInt32(0),
dbus.String(self.path_to_image),
dbus.String(self.title),
dbus.String(text),
actions,
hints,
dbus.UInt32(self.timeout*1000),
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
except Exception as e:
self.notify_another_way(e)
else:
try:
self.notif.Notify(
dbus.String(_('Gajim')),
dbus.String(self.path_to_image),
dbus.UInt32(0),
dbus.String(self.title),
dbus.String(self.text),
dbus.String(''),
hints,
dbus.UInt32(self.timeout*1000),
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
except Exception as e:
self.notify_another_way(e)
def attach_by_id(self, id_):
notification_response_manager.attach_to_interface()
notification_response_manager.add_pending(id_, self)
def notify_another_way(self, e):
app.log.debug('Error when trying to use notification daemon: %s' % \
str(e))
instance = PopupNotificationWindow(self.event_type, self.jid,
self.account, self.msg_type, self.path_to_image, self.title,
self.text, self.timeout)
app.interface.roster.popup_notification_windows.append(instance)
def on_action_invoked(self, id_, reason):
if self.notif is None:
return
self.notif.CloseNotification(dbus.UInt32(id_))
self.notif = None
if reason == 'ignore':
return
app.interface.handle_event(self.account, self.jid, self.msg_type)
def version_reply_handler(self, name, vendor, version, spec_version=None):
if spec_version:
version = spec_version
elif vendor == 'Xfce' and version.startswith('0.1.0'):
version = '0.9'
version_list = version.split('.')
self.version = []
try:
while len(version_list):
self.version.append(int(version_list.pop(0)))
except ValueError:
self.version_error_handler_3_x_try(None)
self.attempt_notify()
def get_version(self):
self.notif.GetServerInfo(
reply_handler=self.version_reply_handler,
error_handler=self.version_error_handler_2_x_try)
def version_error_handler_2_x_try(self, e):
self.notif.GetServerInformation(
reply_handler=self.version_reply_handler,
error_handler=self.version_error_handler_3_x_try)
def version_error_handler_3_x_try(self, e):
self.version = self.default_version
self.attempt_notify()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment