Refactor Notifications

- Use icon names instead of path
- Move PopupNotificationWindow into notify.py
- Make popup class method instead of module method
- Dont use sessions to get control on notification action Fixes #9140
- Add has_focus() method to ChatControlBase
This commit is contained in:
Philipp Hörist 2018-06-01 13:54:04 +02:00
parent 2abbb1e224
commit 4bed8ace95
6 changed files with 349 additions and 409 deletions

View File

@ -1268,6 +1268,13 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
app.log('autoscroll').info('Autoscroll disabled')
self.conv_textview.autoscroll = False
def has_focus(self):
if self.parent_win:
if self.parent_win.window.get_property('has-toplevel-focus'):
if self == self.parent_win.get_active_control():
return True
return False
def _on_scroll(self, widget, event):
if not self.conv_textview.autoscroll:
# autoscroll is already disabled

View File

@ -27,7 +27,6 @@ import hashlib
import hmac
import logging
import sys
import os
from time import time as time_time
import OpenSSL.crypto
@ -40,7 +39,6 @@ from gajim.common import helpers
from gajim.common import app
from gajim.common import i18n
from gajim.common import dataforms
from gajim.common import configpaths
from gajim.common.zeroconf.zeroconf import Constant
from gajim.common.const import KindConstant, SSLError
from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS
@ -2583,8 +2581,50 @@ class GatewayPromptReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
class NotificationEvent(nec.NetworkIncomingEvent):
name = 'notification'
base_network_events = ['decrypted-message-received', 'gc-message-received',
'presence-received']
base_network_events = ['decrypted-message-received',
'gc-message-received',
'presence-received']
def generate(self):
# what's needed to compute output
self.account = self.base_event.conn.name
self.conn = self.base_event.conn
self.jid = ''
self.control = None
self.control_focused = False
self.first_unread = False
# For output
self.do_sound = False
self.sound_file = ''
self.sound_event = '' # gajim sound played if not sound_file is set
self.show_popup = False
self.do_popup = False
self.popup_title = ''
self.popup_text = ''
self.popup_event_type = ''
self.popup_msg_type = ''
self.icon_name = None
self.transport_name = None
self.show = None
self.popup_timeout = -1
self.do_command = False
self.command = ''
self.show_in_notification_area = False
self.show_in_roster = False
self.detect_type()
if self.notif_type == 'msg':
self.handle_incoming_msg_event(self.base_event)
elif self.notif_type == 'gc-msg':
self.handle_incoming_gc_msg_event(self.base_event)
elif self.notif_type == 'pres':
self.handle_incoming_pres_event(self.base_event)
return True
def detect_type(self):
if self.base_event.name == 'decrypted-message-received':
@ -2594,14 +2634,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if self.base_event.name == 'presence-received':
self.notif_type = 'pres'
def get_focused(self):
self.control_focused = False
if self.control:
parent_win = self.control.parent_win
if parent_win and self.control == parent_win.get_active_control() \
and parent_win.window.get_property('has-toplevel-focus'):
self.control_focused = True
def handle_incoming_msg_event(self, msg_obj):
# don't alert for carbon copied messages from ourselves
if msg_obj.sent:
@ -2609,15 +2641,18 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if not msg_obj.msgtxt:
return
self.jid = msg_obj.jid
if msg_obj.session:
self.control = msg_obj.session.control
if msg_obj.mtype == 'pm':
self.jid = msg_obj.fjid
self.control = app.interface.msg_win_mgr.search_control(
msg_obj.jid, self.account, msg_obj.resource)
if self.control is None:
if len(app.events.get_events(
self.account, msg_obj.jid, [msg_obj.mtype])) <= 1:
self.first_unread = True
else:
self.control = None
self.get_focused()
# This event has already been added to event list
if not self.control and len(app.events.get_events(self.conn.name, \
self.jid, [msg_obj.mtype])) <= 1:
self.first_unread = True
self.control_focused = self.control.has_focus()
if msg_obj.mtype == 'pm':
nick = msg_obj.resource
@ -2642,13 +2677,11 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if msg_obj.mtype == 'normal': # single message
self.popup_msg_type = 'normal'
self.popup_event_type = _('New Single Message')
self.popup_image = 'gajim-single_msg_recv'
self.popup_title = _('New Single Message from %(nickname)s') % \
{'nickname': nick}
elif msg_obj.mtype == 'pm':
self.popup_msg_type = 'pm'
self.popup_event_type = _('New Private Message')
self.popup_image = 'gajim-priv_msg_recv'
self.popup_title = _('New Private Message from group chat %s') % \
msg_obj.jid
if self.popup_text:
@ -2660,11 +2693,9 @@ class NotificationEvent(nec.NetworkIncomingEvent):
else: # chat message
self.popup_msg_type = 'chat'
self.popup_event_type = _('New Message')
self.popup_image = 'gajim-chat_msg_recv'
self.popup_title = _('New Message from %(nickname)s') % \
{'nickname': nick}
if app.config.get('notify_on_new_message'):
if self.first_unread or (app.config.get('autopopup_chat_opened') \
and not self.control_focused):
@ -2720,29 +2751,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.do_popup = False
def get_path_to_generic_or_avatar(self, generic, jid=None, suffix=None):
"""
Choose between avatar image and default image
Returns full path to the avatar image if it exists, otherwise returns full
path to the image. generic must be with extension and suffix without
"""
if jid:
# we want an avatar
puny_jid = helpers.sanitize_filename(jid)
path_to_file = os.path.join(
configpaths.get('AVATAR'), puny_jid) + suffix
path_to_local_file = path_to_file + '_local'
for extension in ('.png', '.jpeg'):
path_to_local_file_full = path_to_local_file + extension
if os.path.exists(path_to_local_file_full):
return path_to_local_file_full
for extension in ('.png', '.jpeg'):
path_to_file_full = path_to_file + extension
if os.path.exists(path_to_file_full):
return path_to_file_full
return os.path.abspath(generic)
def handle_incoming_pres_event(self, pres_obj):
if app.jid_is_transport(pres_obj.jid):
return True
@ -2757,7 +2765,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if c.show not in ('offline', 'error'):
return True
# no other resource is connected, let's look in metacontacts
family = app.contacts.get_metacontacts_family(account, self.jid)
for info in family:
@ -2773,8 +2780,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if pres_obj.old_show < 2 and pres_obj.new_show > 1:
event = 'contact_connected'
show_image = 'online.png'
suffix = '_notif_size_colored'
server = app.get_server_from_jid(self.jid)
account_server = account + '/' + server
block_transport = False
@ -2794,8 +2799,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
elif pres_obj.old_show > 1 and pres_obj.new_show < 2:
event = 'contact_disconnected'
show_image = 'offline.png'
suffix = '_notif_size_bw'
if helpers.allow_showing_notification(account, 'notify_on_signout'):
self.do_popup = True
if app.config.get_per('soundevents', 'contact_disconnected',
@ -2805,24 +2808,13 @@ class NotificationEvent(nec.NetworkIncomingEvent):
# Status change (not connected/disconnected or error (<1))
elif pres_obj.new_show > 1:
event = 'status_change'
# FIXME: we don't always 'online.png', but we first need 48x48 for
# all status
show_image = 'online.png'
suffix = '_notif_size_colored'
else:
return True
transport_name = app.get_transport_name_from_jid(self.jid)
img_path = None
if transport_name:
img_path = os.path.join(helpers.get_transport_path(
transport_name), '48x48', show_image)
if not img_path or not os.path.isfile(img_path):
iconset = app.config.get('iconset')
img_path = os.path.join(helpers.get_iconset_path(iconset),
'48x48', show_image)
self.popup_image_path = self.get_path_to_generic_or_avatar(img_path,
jid=self.jid, suffix=suffix)
if app.jid_is_transport(self.jid):
self.transport_name = app.get_transport_name_from_jid(self.jid)
self.show = pres_obj.show
self.popup_timeout = app.config.get('notification_timeout')
@ -2848,45 +2840,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.popup_text = pres_obj.status
self.popup_event_type = _('Contact Signed Out')
def generate(self):
# what's needed to compute output
self.conn = self.base_event.conn
self.jid = ''
self.control = None
self.control_focused = False
self.first_unread = False
# For output
self.do_sound = False
self.sound_file = ''
self.sound_event = '' # gajim sound played if not sound_file is set
self.show_popup = False
self.do_popup = False
self.popup_title = ''
self.popup_text = ''
self.popup_event_type = ''
self.popup_msg_type = ''
self.popup_image = ''
self.popup_image_path = ''
self.popup_timeout = -1
self.do_command = False
self.command = ''
self.show_in_notification_area = False
self.show_in_roster = False
self.detect_type()
if self.notif_type == 'msg':
self.handle_incoming_msg_event(self.base_event)
elif self.notif_type == 'gc-msg':
self.handle_incoming_gc_msg_event(self.base_event)
elif self.notif_type == 'pres':
self.handle_incoming_pres_event(self.base_event)
return True
class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
name = 'message-outgoing'
base_network_events = []

View File

@ -3060,131 +3060,6 @@ class ChangePasswordDialog:
dialog.destroy()
self.on_response(password1)
class PopupNotificationWindow:
def __init__(self, event_type, jid, account, msg_type='',
path_to_image=None, title=None, text=None, timeout=-1):
self.account = account
self.jid = jid
self.msg_type = msg_type
self.index = len(app.interface.roster.popup_notification_windows)
xml = gtkgui_helpers.get_gtk_builder('popup_notification_window.ui')
self.window = xml.get_object('popup_notification_window')
self.window.set_type_hint(Gdk.WindowTypeHint.TOOLTIP)
self.window.set_name('NotificationPopup')
close_button = xml.get_object('close_button')
event_type_label = xml.get_object('event_type_label')
event_description_label = xml.get_object('event_description_label')
eventbox = xml.get_object('eventbox')
image = xml.get_object('notification_image')
if not text:
text = app.get_name_from_jid(account, jid) # default value of text
if not title:
title = ''
event_type_label.set_markup(
'<span foreground="black" weight="bold">%s</span>' %
GLib.markup_escape_text(title))
css = '#NotificationPopup {background-color: black }'
gtkgui_helpers.add_css_to_widget(self.window, css)
# default image
if not path_to_image:
path_to_image = gtkgui_helpers.get_icon_path('gajim-chat_msg_recv', 48)
if event_type == _('Contact Signed In'):
bg_color = app.config.get('notif_signin_color')
elif event_type == _('Contact Signed Out'):
bg_color = app.config.get('notif_signout_color')
elif event_type in (_('New Message'), _('New Single Message'),
_('New Private Message'), _('New E-mail')):
bg_color = app.config.get('notif_message_color')
elif event_type == _('File Transfer Request'):
bg_color = app.config.get('notif_ftrequest_color')
elif event_type == _('File Transfer Error'):
bg_color = app.config.get('notif_fterror_color')
elif event_type in (_('File Transfer Completed'),
_('File Transfer Stopped')):
bg_color = app.config.get('notif_ftcomplete_color')
elif event_type == _('Groupchat Invitation'):
bg_color = app.config.get('notif_invite_color')
elif event_type == _('Contact Changed Status'):
bg_color = app.config.get('notif_status_color')
else: # Unknown event! Shouldn't happen but deal with it
bg_color = app.config.get('notif_other_color')
background_class = '''
.popup-style {
border-image: none;
background-image: none;
background-color: %s }''' % bg_color
gtkgui_helpers.add_css_to_widget(eventbox, background_class)
eventbox.get_style_context().add_class('popup-style')
gtkgui_helpers.add_css_to_widget(close_button, background_class)
eventbox.get_style_context().add_class('popup-style')
event_description_label.set_markup('<span foreground="black">%s</span>' %
GLib.markup_escape_text(text))
# set the image
image.set_from_file(path_to_image)
# position the window to bottom-right of screen
window_width, self.window_height = self.window.get_size()
app.interface.roster.popups_notification_height += self.window_height
pos_x = app.config.get('notification_position_x')
screen_w, screen_h = gtkgui_helpers.get_total_screen_geometry()
if pos_x < 0:
pos_x = screen_w - window_width + pos_x + 1
pos_y = app.config.get('notification_position_y')
if pos_y < 0:
pos_y = screen_h - \
app.interface.roster.popups_notification_height + pos_y + 1
self.window.move(pos_x, pos_y)
xml.connect_signals(self)
self.window.show_all()
if timeout > 0:
GLib.timeout_add_seconds(timeout, self.on_timeout)
def on_close_button_clicked(self, widget):
self.adjust_height_and_move_popup_notification_windows()
def on_timeout(self):
self.adjust_height_and_move_popup_notification_windows()
def adjust_height_and_move_popup_notification_windows(self):
#remove
app.interface.roster.popups_notification_height -= self.window_height
self.window.destroy()
if len(app.interface.roster.popup_notification_windows) > self.index:
# we want to remove the destroyed window from the list
app.interface.roster.popup_notification_windows.pop(self.index)
# move the rest of popup windows
app.interface.roster.popups_notification_height = 0
current_index = 0
for window_instance in app.interface.roster.popup_notification_windows:
window_instance.index = current_index
current_index += 1
window_width, window_height = window_instance.window.get_size()
app.interface.roster.popups_notification_height += window_height
screen_w, screen_h = gtkgui_helpers.get_total_screen_geometry()
window_instance.window.move(screen_w - window_width,
screen_h - \
app.interface.roster.popups_notification_height)
def on_popup_notification_window_button_press_event(self, widget, event):
if event.button != 1:
self.window.destroy()
return
app.interface.handle_event(self.account, self.jid, self.msg_type)
self.adjust_height_and_move_popup_notification_windows()
class SingleMessageWindow:
"""
SingleMessageWindow can send or show a received singled message depending on

View File

@ -920,3 +920,11 @@ def pango_to_css_weight(number):
if number > 900:
return 900
return int(math.ceil(number / 100.0)) * 100
def get_monitor_scale_factor():
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor()
if monitor is None:
log.warning('Could not determine scale factor')
return 1
return monitor.get_scale_factor()

View File

@ -226,10 +226,10 @@ class Interface:
@staticmethod
def handle_event_connection_lost(obj):
# ('CONNECTION_LOST', account, [title, text])
path = gtkgui_helpers.get_icon_path('gajim-connection_lost', 48)
account = obj.conn.name
notify.popup(_('Connection Failed'), account, account,
'connection-lost', path, obj.title, obj.msg)
app.notification.popup(
_('Connection Failed'), account, account,
'connection-lost', 'gajim-connection_lost', obj.title, obj.msg)
@staticmethod
def unblock_signed_in_notifications(account):
@ -509,11 +509,10 @@ class Interface:
self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-subscription_request',
48)
event_type = _('Subscription request')
notify.popup(event_type, obj.jid, account, 'subscription_request',
path, event_type, obj.jid)
app.notification.popup(
event_type, obj.jid, account, 'subscription_request',
'gajim-subscription_request', event_type, obj.jid)
def handle_event_subscribed_presence(self, obj):
#('SUBSCRIBED', account, (jid, resource))
@ -567,9 +566,10 @@ class Interface:
self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-unsubscribed', 48)
event_type = _('Unsubscribed')
notify.popup(event_type, obj.jid, account, 'unsubscribed', path,
app.notification.popup(
event_type, obj.jid, account,
'unsubscribed', 'gajim-unsubscribed',
event_type, obj.jid)
@staticmethod
@ -660,10 +660,10 @@ class Interface:
self.add_event(account, obj.jid_from, event)
if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-gc_invitation', 48)
event_type = _('Groupchat Invitation')
notify.popup(event_type, obj.jid_from, account, 'gc-invitation',
path, event_type, obj.room_jid)
app.notification.popup(
event_type, obj.jid_from, account, 'gc-invitation',
'gajim-gc_invitation', event_type, obj.room_jid)
def forget_gpg_passphrase(self, keyid):
if keyid in self.gpg_passphrase:
@ -680,9 +680,9 @@ class Interface:
'key.')
dialogs.WarningDialog(_('Wrong passphrase'), sectext)
else:
path = gtkgui_helpers.get_icon_path('gtk-dialog-warning', 48)
account = obj.conn.name
notify.popup('warning', account, account, '', path,
app.notification.popup(
'warning', account, account, '', 'dialog-warning',
_('Wrong OpenPGP passphrase'),
_('You are currently connected without your OpenPGP key.'))
self.forget_gpg_passphrase(obj.keyID)
@ -858,9 +858,10 @@ class Interface:
self.add_event(account, jid, event)
if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
event_type = _('File Transfer Error')
notify.popup(event_type, jid, account, 'file-send-error', path,
app.notification.popup(
event_type, jid, account,
'file-send-error', 'gajim-ft_error',
event_type, file_props.name)
def handle_event_file_request_error(self, obj):
@ -888,9 +889,10 @@ class Interface:
if helpers.allow_showing_notification(obj.conn.name):
# check if we should be notified
path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
event_type = _('File Transfer Error')
notify.popup(event_type, obj.jid, obj.conn.name, msg_type, path,
app.notification.popup(
event_type, obj.jid, obj.conn.name,
msg_type, 'gajim-ft_error',
title=event_type, text=obj.file_props.name)
def handle_event_file_request(self, obj):
@ -921,12 +923,12 @@ class Interface:
event = events.FileRequestEvent(obj.file_props)
self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-ft_request', 48)
txt = _('%s wants to send you a file.') % app.get_name_from_jid(
account, obj.jid)
event_type = _('File Transfer Request')
notify.popup(event_type, obj.jid, account, 'file-request',
path_to_image=path, title=event_type, text=txt)
app.notification.popup(
event_type, obj.jid, account, 'file-request',
icon_name='gajim-ft_request', title=event_type, text=txt)
@staticmethod
def handle_event_file_error(title, message):
@ -1060,15 +1062,15 @@ class Interface:
if event_type == _('File Transfer Completed'):
txt = _('%(filename)s received from %(name)s.')\
% {'filename': filename, 'name': name}
img_name = 'gajim-ft_done'
icon_name = 'gajim-ft_done'
elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s from %(name)s '
'stopped.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped'
icon_name = 'gajim-ft_stopped'
else: # ft hash error
txt = _('File transfer of %(filename)s from %(name)s '
'failed.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped'
icon_name = 'gajim-ft_stopped'
else:
receiver = file_props.receiver
if hasattr(receiver, 'jid'):
@ -1081,27 +1083,27 @@ class Interface:
if event_type == _('File Transfer Completed'):
txt = _('You successfully sent %(filename)s to %(name)s.')\
% {'filename': filename, 'name': name}
img_name = 'gajim-ft_done'
icon_name = 'gajim-ft_done'
elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s to %(name)s '
'stopped.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped'
icon_name = 'gajim-ft_stopped'
else: # ft hash error
txt = _('File transfer of %(filename)s to %(name)s '
'failed.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped'
path = gtkgui_helpers.get_icon_path(img_name, 48)
icon_name = 'gajim-ft_stopped'
else:
txt = ''
path = ''
icon_name = None
if app.config.get('notify_on_file_complete') and \
(app.config.get('autopopupaway') or \
app.connections[account].connected in (2, 3)):
# we want to be notified and we are online/chat or we don't mind
# bugged when away/na/busy
notify.popup(event_type, jid, account, msg_type, path_to_image=path,
title=event_type, text=txt)
app.notification.popup(
event_type, jid, account, msg_type,
icon_name=icon_name, title=event_type, text=txt)
def handle_event_signed_in(self, obj):
"""
@ -1278,10 +1280,10 @@ class Interface:
# TODO: we should use another pixmap ;-)
txt = _('%s wants to start a voice chat.') % \
app.get_name_from_jid(account, obj.fjid)
path = gtkgui_helpers.get_icon_path('gajim-mic_active', 48)
event_type = _('Voice Chat Request')
notify.popup(event_type, obj.fjid, account, 'jingle-incoming',
path_to_image=path, title=event_type, text=txt)
app.notification.popup(
event_type, obj.fjid, account, 'jingle-incoming',
icon_name='gajim-mic_active', title=event_type, text=txt)
def handle_event_jingle_connected(self, obj):
# ('JINGLE_CONNECTED', account, (peerjid, sid, media))
@ -1652,28 +1654,7 @@ class Interface:
elif type_ in ('printed_chat', 'chat', ''):
# '' is for log in/out notifications
if type_ != '':
event = app.events.get_first_event(account, fjid, type_)
if not event:
event = app.events.get_first_event(account, jid, type_)
if not event:
# If autopopup_chat_opened = True, then we send out
# notifications even if a control is open. This means the
# event is already deleted (because its printed to the
# control) when the notification is clicked. So try to
# get a control from account/jid
ctrl = self.msg_win_mgr.get_control(fjid, account)
if ctrl is None:
return
w = ctrl.parent_win
if type_ == 'printed_chat':
ctrl = event.control
elif type_ == 'chat':
session = event.session
ctrl = session.control
elif type_ == '':
ctrl = self.msg_win_mgr.get_control(fjid, account)
ctrl = self.msg_win_mgr.search_control(jid, account, resource)
if not ctrl:
highest_contact = app.contacts.\
@ -1692,33 +1673,20 @@ class Interface:
if not contact:
contact = highest_contact
ctrl = self.new_chat(contact, account, resource=resource,
session=session)
ctrl = self.new_chat(contact, account, resource=resource)
app.last_message_time[account][jid] = 0 # long time ago
w = ctrl.parent_win
elif type_ in ('printed_pm', 'pm'):
# assume that the most recently updated control we have for this
# party is the one that this event was in
event = app.events.get_first_event(account, fjid, type_)
if not event:
event = app.events.get_first_event(account, jid, type_)
if not event:
return
if type_ == 'printed_pm':
ctrl = event.control
elif type_ == 'pm':
session = event.session
ctrl = self.msg_win_mgr.get_control(fjid, account)
if session and session.control:
ctrl = session.control
elif not ctrl:
if not ctrl:
room_jid = jid
nick = resource
gc_contact = app.contacts.get_gc_contact(account, room_jid,
nick)
gc_contact = app.contacts.get_gc_contact(
account, room_jid, nick)
if gc_contact:
show = gc_contact.show
else:
@ -1727,12 +1695,7 @@ class Interface:
room_jid=room_jid, account=account, name=nick,
show=show)
if not session:
session = app.connections[account].make_new_session(
fjid, None, type_='pm')
self.new_private_chat(gc_contact, account, session=session)
ctrl = session.control
ctrl = self.new_private_chat(gc_contact, account)
w = ctrl.parent_win
elif type_ in ('normal', 'file-request', 'file-request-error',

View File

@ -1,42 +1,43 @@
# -*- coding:utf-8 -*-
## src/notify.py
##
## Copyright (C) 2005 Sebastian Estienne
## Copyright (C) 2005-2006 Andrew Sayman <lorien420 AT myrealbox.com>
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
## Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
## Jonathan Schleifer <js-gajim AT webkeks.org>
##
## This file is part of Gajim.
##
## Gajim 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 3 only.
##
## Gajim 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.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
#
# Copyright (C) 2005 Sebastian Estienne
# Copyright (C) 2005-2006 Andrew Sayman <lorien420 AT myrealbox.com>
# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim 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 3 only.
#
# Gajim 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.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import sys
from gajim.dialogs import PopupNotificationWindow
from gi.repository import GLib
from gi.repository import Gio
from gajim import gtkgui_helpers
from gi.repository import Gdk
from gi.repository import Gtk
from gajim import gtkgui_helpers
from gajim.common import app
from gajim.common import helpers
from gajim.common import ged
def get_show_in_roster(event, account, contact, session=None):
"""
Return True if this event must be shown in roster, else False
@ -48,91 +49,37 @@ def get_show_in_roster(event, account, contact, session=None):
return False
return True
def get_show_in_systray(event, account, contact, type_=None):
"""
Return True if this event must be shown in systray, else False
"""
if type_ == 'printed_gc_msg' and not app.config.get(
'notify_on_all_muc_messages') and not app.config.get_per('rooms',
contact.jid, 'notify_on_all_messages'):
notify = app.config.get('notify_on_all_muc_messages')
notify_for_jid = app.config.get_per(
'rooms', contact.jid, 'notify_on_all_messages')
if type_ == 'printed_gc_msg' and not notify and not notify_for_jid:
# it's not an highlighted message, don't show in systray
return False
return app.config.get('trayicon_notification_on_events')
def popup(event_type, jid, account, type_='', path_to_image=None, title=None,
text=None, timeout=-1):
"""
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:
path_to_image = gtkgui_helpers.get_icon_path('gajim-chat_msg_recv', 48)
if timeout < 0:
timeout = app.config.get('notification_timeout')
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)
#Button in notification
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)
app.app.send_notification(notif_id, notification)
class Notification:
"""
Handle notifications
"""
def __init__(self):
app.ged.register_event_handler('notification', ged.GUI2,
self._nec_notification)
app.ged.register_event_handler(
'notification', ged.GUI2, self._nec_notification)
def _nec_notification(self, obj):
if obj.do_popup:
if obj.popup_image:
icon_path = gtkgui_helpers.get_icon_path(obj.popup_image, 48)
if icon_path:
image_path = icon_path
elif obj.popup_image_path:
image_path = obj.popup_image_path
else:
image_path = ''
popup(obj.popup_event_type, obj.jid, obj.conn.name,
obj.popup_msg_type, path_to_image=image_path,
title=obj.popup_title, text=obj.popup_text,
timeout=obj.popup_timeout)
icon_name = self._get_icon_name(obj)
self.popup(obj.popup_event_type, obj.jid, obj.conn.name,
obj.popup_msg_type, icon_name=icon_name,
title=obj.popup_title, text=obj.popup_text,
timeout=obj.popup_timeout)
if obj.do_sound:
if obj.sound_file:
@ -145,3 +92,190 @@ class Notification:
helpers.exec_command(obj.command, use_shell=True)
except Exception:
pass
def _get_icon_name(self, obj):
if obj.notif_type == 'msg':
if obj.base_event.mtype == 'pm':
return 'gajim-priv_msg_recv'
if obj.base_event.mtype == 'normal':
return 'gajim-single_msg_recv'
elif obj.notif_type == 'pres':
if obj.transport_name is not None:
return '%s-%s' % (obj.transport_name, obj.show)
else:
return gtkgui_helpers.get_iconset_name_for(obj.show)
def popup(self, event_type, jid, account, type_='', icon_name=None,
title=None, text=None, timeout=-1):
"""
Notify a user of an event using GNotification and GApplication under
Linux, Use PopupNotificationWindow under Windows
"""
if icon_name is None:
icon_name = 'gajim-chat_msg_recv'
if timeout < 0:
timeout = app.config.get('notification_timeout')
if sys.platform == 'win32':
instance = PopupNotificationWindow(event_type, jid, account, type_,
icon_name, title, text, timeout)
app.interface.roster.popup_notification_windows.append(instance)
return
scale = app.get_monitor_scale_factor()
icon_pixbuf = gtkgui_helpers.gtk_icon_theme.load_icon_for_scale(
icon_name, 48, scale, 0)
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_pixbuf)
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)
#Button in notification
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)
app.app.send_notification(notif_id, notification)
class PopupNotificationWindow:
def __init__(self, event_type, jid, account, msg_type='',
icon_name=None, title=None, text=None, timeout=-1):
self.account = account
self.jid = jid
self.msg_type = msg_type
self.index = len(app.interface.roster.popup_notification_windows)
xml = gtkgui_helpers.get_gtk_builder('popup_notification_window.ui')
self.window = xml.get_object('popup_notification_window')
self.window.set_type_hint(Gdk.WindowTypeHint.TOOLTIP)
self.window.set_name('NotificationPopup')
close_button = xml.get_object('close_button')
event_type_label = xml.get_object('event_type_label')
event_description_label = xml.get_object('event_description_label')
eventbox = xml.get_object('eventbox')
image = xml.get_object('notification_image')
if not text:
text = app.get_name_from_jid(account, jid) # default value of text
if not title:
title = ''
event_type_label.set_markup(
'<span foreground="black" weight="bold">%s</span>' %
GLib.markup_escape_text(title))
css = '#NotificationPopup {background-color: black }'
gtkgui_helpers.add_css_to_widget(self.window, css)
if event_type == _('Contact Signed In'):
bg_color = app.config.get('notif_signin_color')
elif event_type == _('Contact Signed Out'):
bg_color = app.config.get('notif_signout_color')
elif event_type in (_('New Message'), _('New Single Message'),
_('New Private Message'), _('New E-mail')):
bg_color = app.config.get('notif_message_color')
elif event_type == _('File Transfer Request'):
bg_color = app.config.get('notif_ftrequest_color')
elif event_type == _('File Transfer Error'):
bg_color = app.config.get('notif_fterror_color')
elif event_type in (_('File Transfer Completed'),
_('File Transfer Stopped')):
bg_color = app.config.get('notif_ftcomplete_color')
elif event_type == _('Groupchat Invitation'):
bg_color = app.config.get('notif_invite_color')
elif event_type == _('Contact Changed Status'):
bg_color = app.config.get('notif_status_color')
else: # Unknown event! Shouldn't happen but deal with it
bg_color = app.config.get('notif_other_color')
background_class = '''
.popup-style {
border-image: none;
background-image: none;
background-color: %s }''' % bg_color
gtkgui_helpers.add_css_to_widget(eventbox, background_class)
eventbox.get_style_context().add_class('popup-style')
gtkgui_helpers.add_css_to_widget(close_button, background_class)
eventbox.get_style_context().add_class('popup-style')
event_description_label.set_markup('<span foreground="black">%s</span>' %
GLib.markup_escape_text(text))
# set the image
image.set_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
# position the window to bottom-right of screen
window_width, self.window_height = self.window.get_size()
app.interface.roster.popups_notification_height += self.window_height
pos_x = app.config.get('notification_position_x')
screen_w, screen_h = gtkgui_helpers.get_total_screen_geometry()
if pos_x < 0:
pos_x = screen_w - window_width + pos_x + 1
pos_y = app.config.get('notification_position_y')
if pos_y < 0:
pos_y = screen_h - \
app.interface.roster.popups_notification_height + pos_y + 1
self.window.move(pos_x, pos_y)
xml.connect_signals(self)
self.window.show_all()
if timeout > 0:
GLib.timeout_add_seconds(timeout, self.on_timeout)
def on_close_button_clicked(self, widget):
self.adjust_height_and_move_popup_notification_windows()
def on_timeout(self):
self.adjust_height_and_move_popup_notification_windows()
def adjust_height_and_move_popup_notification_windows(self):
#remove
app.interface.roster.popups_notification_height -= self.window_height
self.window.destroy()
if len(app.interface.roster.popup_notification_windows) > self.index:
# we want to remove the destroyed window from the list
app.interface.roster.popup_notification_windows.pop(self.index)
# move the rest of popup windows
app.interface.roster.popups_notification_height = 0
current_index = 0
for window_instance in app.interface.roster.popup_notification_windows:
window_instance.index = current_index
current_index += 1
window_width, window_height = window_instance.window.get_size()
app.interface.roster.popups_notification_height += window_height
screen_w, screen_h = gtkgui_helpers.get_total_screen_geometry()
window_instance.window.move(screen_w - window_width,
screen_h - \
app.interface.roster.popups_notification_height)
def on_popup_notification_window_button_press_event(self, widget, event):
if event.button != 1:
self.window.destroy()
return
app.interface.handle_event(self.account, self.jid, self.msg_type)
self.adjust_height_and_move_popup_notification_windows()