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') app.log('autoscroll').info('Autoscroll disabled')
self.conv_textview.autoscroll = False 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): def _on_scroll(self, widget, event):
if not self.conv_textview.autoscroll: if not self.conv_textview.autoscroll:
# autoscroll is already disabled # autoscroll is already disabled

View File

@ -27,7 +27,6 @@ import hashlib
import hmac import hmac
import logging import logging
import sys import sys
import os
from time import time as time_time from time import time as time_time
import OpenSSL.crypto import OpenSSL.crypto
@ -40,7 +39,6 @@ from gajim.common import helpers
from gajim.common import app from gajim.common import app
from gajim.common import i18n from gajim.common import i18n
from gajim.common import dataforms from gajim.common import dataforms
from gajim.common import configpaths
from gajim.common.zeroconf.zeroconf import Constant from gajim.common.zeroconf.zeroconf import Constant
from gajim.common.const import KindConstant, SSLError from gajim.common.const import KindConstant, SSLError
from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS
@ -2583,8 +2581,50 @@ class GatewayPromptReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
class NotificationEvent(nec.NetworkIncomingEvent): class NotificationEvent(nec.NetworkIncomingEvent):
name = 'notification' name = 'notification'
base_network_events = ['decrypted-message-received', 'gc-message-received', base_network_events = ['decrypted-message-received',
'presence-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): def detect_type(self):
if self.base_event.name == 'decrypted-message-received': if self.base_event.name == 'decrypted-message-received':
@ -2594,14 +2634,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if self.base_event.name == 'presence-received': if self.base_event.name == 'presence-received':
self.notif_type = 'pres' 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): def handle_incoming_msg_event(self, msg_obj):
# don't alert for carbon copied messages from ourselves # don't alert for carbon copied messages from ourselves
if msg_obj.sent: if msg_obj.sent:
@ -2609,15 +2641,18 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if not msg_obj.msgtxt: if not msg_obj.msgtxt:
return return
self.jid = msg_obj.jid self.jid = msg_obj.jid
if msg_obj.session: if msg_obj.mtype == 'pm':
self.control = msg_obj.session.control 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: else:
self.control = None self.control_focused = self.control.has_focus()
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
if msg_obj.mtype == 'pm': if msg_obj.mtype == 'pm':
nick = msg_obj.resource nick = msg_obj.resource
@ -2642,13 +2677,11 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if msg_obj.mtype == 'normal': # single message if msg_obj.mtype == 'normal': # single message
self.popup_msg_type = 'normal' self.popup_msg_type = 'normal'
self.popup_event_type = _('New Single Message') self.popup_event_type = _('New Single Message')
self.popup_image = 'gajim-single_msg_recv'
self.popup_title = _('New Single Message from %(nickname)s') % \ self.popup_title = _('New Single Message from %(nickname)s') % \
{'nickname': nick} {'nickname': nick}
elif msg_obj.mtype == 'pm': elif msg_obj.mtype == 'pm':
self.popup_msg_type = 'pm' self.popup_msg_type = 'pm'
self.popup_event_type = _('New Private Message') self.popup_event_type = _('New Private Message')
self.popup_image = 'gajim-priv_msg_recv'
self.popup_title = _('New Private Message from group chat %s') % \ self.popup_title = _('New Private Message from group chat %s') % \
msg_obj.jid msg_obj.jid
if self.popup_text: if self.popup_text:
@ -2660,11 +2693,9 @@ class NotificationEvent(nec.NetworkIncomingEvent):
else: # chat message else: # chat message
self.popup_msg_type = 'chat' self.popup_msg_type = 'chat'
self.popup_event_type = _('New Message') self.popup_event_type = _('New Message')
self.popup_image = 'gajim-chat_msg_recv'
self.popup_title = _('New Message from %(nickname)s') % \ self.popup_title = _('New Message from %(nickname)s') % \
{'nickname': nick} {'nickname': nick}
if app.config.get('notify_on_new_message'): if app.config.get('notify_on_new_message'):
if self.first_unread or (app.config.get('autopopup_chat_opened') \ if self.first_unread or (app.config.get('autopopup_chat_opened') \
and not self.control_focused): and not self.control_focused):
@ -2720,29 +2751,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.do_popup = False 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): def handle_incoming_pres_event(self, pres_obj):
if app.jid_is_transport(pres_obj.jid): if app.jid_is_transport(pres_obj.jid):
return True return True
@ -2757,7 +2765,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if c.show not in ('offline', 'error'): if c.show not in ('offline', 'error'):
return True return True
# no other resource is connected, let's look in metacontacts # no other resource is connected, let's look in metacontacts
family = app.contacts.get_metacontacts_family(account, self.jid) family = app.contacts.get_metacontacts_family(account, self.jid)
for info in family: for info in family:
@ -2773,8 +2780,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
if pres_obj.old_show < 2 and pres_obj.new_show > 1: if pres_obj.old_show < 2 and pres_obj.new_show > 1:
event = 'contact_connected' event = 'contact_connected'
show_image = 'online.png'
suffix = '_notif_size_colored'
server = app.get_server_from_jid(self.jid) server = app.get_server_from_jid(self.jid)
account_server = account + '/' + server account_server = account + '/' + server
block_transport = False block_transport = False
@ -2794,8 +2799,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
elif pres_obj.old_show > 1 and pres_obj.new_show < 2: elif pres_obj.old_show > 1 and pres_obj.new_show < 2:
event = 'contact_disconnected' event = 'contact_disconnected'
show_image = 'offline.png'
suffix = '_notif_size_bw'
if helpers.allow_showing_notification(account, 'notify_on_signout'): if helpers.allow_showing_notification(account, 'notify_on_signout'):
self.do_popup = True self.do_popup = True
if app.config.get_per('soundevents', 'contact_disconnected', if app.config.get_per('soundevents', 'contact_disconnected',
@ -2805,24 +2808,13 @@ class NotificationEvent(nec.NetworkIncomingEvent):
# Status change (not connected/disconnected or error (<1)) # Status change (not connected/disconnected or error (<1))
elif pres_obj.new_show > 1: elif pres_obj.new_show > 1:
event = 'status_change' 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: else:
return True return True
transport_name = app.get_transport_name_from_jid(self.jid) if app.jid_is_transport(self.jid):
img_path = None self.transport_name = app.get_transport_name_from_jid(self.jid)
if transport_name:
img_path = os.path.join(helpers.get_transport_path( self.show = pres_obj.show
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)
self.popup_timeout = app.config.get('notification_timeout') self.popup_timeout = app.config.get('notification_timeout')
@ -2848,45 +2840,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.popup_text = pres_obj.status self.popup_text = pres_obj.status
self.popup_event_type = _('Contact Signed Out') 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): class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
name = 'message-outgoing' name = 'message-outgoing'
base_network_events = [] base_network_events = []

View File

@ -3060,131 +3060,6 @@ class ChangePasswordDialog:
dialog.destroy() dialog.destroy()
self.on_response(password1) 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: class SingleMessageWindow:
""" """
SingleMessageWindow can send or show a received singled message depending on 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: if number > 900:
return 900 return 900
return int(math.ceil(number / 100.0)) * 100 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 @staticmethod
def handle_event_connection_lost(obj): def handle_event_connection_lost(obj):
# ('CONNECTION_LOST', account, [title, text]) # ('CONNECTION_LOST', account, [title, text])
path = gtkgui_helpers.get_icon_path('gajim-connection_lost', 48)
account = obj.conn.name account = obj.conn.name
notify.popup(_('Connection Failed'), account, account, app.notification.popup(
'connection-lost', path, obj.title, obj.msg) _('Connection Failed'), account, account,
'connection-lost', 'gajim-connection_lost', obj.title, obj.msg)
@staticmethod @staticmethod
def unblock_signed_in_notifications(account): def unblock_signed_in_notifications(account):
@ -509,11 +509,10 @@ class Interface:
self.add_event(account, obj.jid, event) self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account): if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-subscription_request',
48)
event_type = _('Subscription request') event_type = _('Subscription request')
notify.popup(event_type, obj.jid, account, 'subscription_request', app.notification.popup(
path, event_type, obj.jid) event_type, obj.jid, account, 'subscription_request',
'gajim-subscription_request', event_type, obj.jid)
def handle_event_subscribed_presence(self, obj): def handle_event_subscribed_presence(self, obj):
#('SUBSCRIBED', account, (jid, resource)) #('SUBSCRIBED', account, (jid, resource))
@ -567,9 +566,10 @@ class Interface:
self.add_event(account, obj.jid, event) self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account): if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-unsubscribed', 48)
event_type = _('Unsubscribed') 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) event_type, obj.jid)
@staticmethod @staticmethod
@ -660,10 +660,10 @@ class Interface:
self.add_event(account, obj.jid_from, event) self.add_event(account, obj.jid_from, event)
if helpers.allow_showing_notification(account): if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-gc_invitation', 48)
event_type = _('Groupchat Invitation') event_type = _('Groupchat Invitation')
notify.popup(event_type, obj.jid_from, account, 'gc-invitation', app.notification.popup(
path, event_type, obj.room_jid) event_type, obj.jid_from, account, 'gc-invitation',
'gajim-gc_invitation', event_type, obj.room_jid)
def forget_gpg_passphrase(self, keyid): def forget_gpg_passphrase(self, keyid):
if keyid in self.gpg_passphrase: if keyid in self.gpg_passphrase:
@ -680,9 +680,9 @@ class Interface:
'key.') 'key.')
dialogs.WarningDialog(_('Wrong passphrase'), sectext) dialogs.WarningDialog(_('Wrong passphrase'), sectext)
else: else:
path = gtkgui_helpers.get_icon_path('gtk-dialog-warning', 48)
account = obj.conn.name account = obj.conn.name
notify.popup('warning', account, account, '', path, app.notification.popup(
'warning', account, account, '', 'dialog-warning',
_('Wrong OpenPGP passphrase'), _('Wrong OpenPGP passphrase'),
_('You are currently connected without your OpenPGP key.')) _('You are currently connected without your OpenPGP key.'))
self.forget_gpg_passphrase(obj.keyID) self.forget_gpg_passphrase(obj.keyID)
@ -858,9 +858,10 @@ class Interface:
self.add_event(account, jid, event) self.add_event(account, jid, event)
if helpers.allow_showing_notification(account): if helpers.allow_showing_notification(account):
path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
event_type = _('File Transfer Error') 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) event_type, file_props.name)
def handle_event_file_request_error(self, obj): def handle_event_file_request_error(self, obj):
@ -888,9 +889,10 @@ class Interface:
if helpers.allow_showing_notification(obj.conn.name): if helpers.allow_showing_notification(obj.conn.name):
# check if we should be notified # check if we should be notified
path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
event_type = _('File Transfer Error') 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) title=event_type, text=obj.file_props.name)
def handle_event_file_request(self, obj): def handle_event_file_request(self, obj):
@ -921,12 +923,12 @@ class Interface:
event = events.FileRequestEvent(obj.file_props) event = events.FileRequestEvent(obj.file_props)
self.add_event(account, obj.jid, event) self.add_event(account, obj.jid, event)
if helpers.allow_showing_notification(account): 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( txt = _('%s wants to send you a file.') % app.get_name_from_jid(
account, obj.jid) account, obj.jid)
event_type = _('File Transfer Request') event_type = _('File Transfer Request')
notify.popup(event_type, obj.jid, account, 'file-request', app.notification.popup(
path_to_image=path, title=event_type, text=txt) event_type, obj.jid, account, 'file-request',
icon_name='gajim-ft_request', title=event_type, text=txt)
@staticmethod @staticmethod
def handle_event_file_error(title, message): def handle_event_file_error(title, message):
@ -1060,15 +1062,15 @@ class Interface:
if event_type == _('File Transfer Completed'): if event_type == _('File Transfer Completed'):
txt = _('%(filename)s received from %(name)s.')\ txt = _('%(filename)s received from %(name)s.')\
% {'filename': filename, 'name': name} % {'filename': filename, 'name': name}
img_name = 'gajim-ft_done' icon_name = 'gajim-ft_done'
elif event_type == _('File Transfer Stopped'): elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s from %(name)s ' txt = _('File transfer of %(filename)s from %(name)s '
'stopped.') % {'filename': filename, 'name': name} 'stopped.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped' icon_name = 'gajim-ft_stopped'
else: # ft hash error else: # ft hash error
txt = _('File transfer of %(filename)s from %(name)s ' txt = _('File transfer of %(filename)s from %(name)s '
'failed.') % {'filename': filename, 'name': name} 'failed.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped' icon_name = 'gajim-ft_stopped'
else: else:
receiver = file_props.receiver receiver = file_props.receiver
if hasattr(receiver, 'jid'): if hasattr(receiver, 'jid'):
@ -1081,27 +1083,27 @@ class Interface:
if event_type == _('File Transfer Completed'): if event_type == _('File Transfer Completed'):
txt = _('You successfully sent %(filename)s to %(name)s.')\ txt = _('You successfully sent %(filename)s to %(name)s.')\
% {'filename': filename, 'name': name} % {'filename': filename, 'name': name}
img_name = 'gajim-ft_done' icon_name = 'gajim-ft_done'
elif event_type == _('File Transfer Stopped'): elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s to %(name)s ' txt = _('File transfer of %(filename)s to %(name)s '
'stopped.') % {'filename': filename, 'name': name} 'stopped.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped' icon_name = 'gajim-ft_stopped'
else: # ft hash error else: # ft hash error
txt = _('File transfer of %(filename)s to %(name)s ' txt = _('File transfer of %(filename)s to %(name)s '
'failed.') % {'filename': filename, 'name': name} 'failed.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped' icon_name = 'gajim-ft_stopped'
path = gtkgui_helpers.get_icon_path(img_name, 48)
else: else:
txt = '' txt = ''
path = '' icon_name = None
if app.config.get('notify_on_file_complete') and \ if app.config.get('notify_on_file_complete') and \
(app.config.get('autopopupaway') or \ (app.config.get('autopopupaway') or \
app.connections[account].connected in (2, 3)): app.connections[account].connected in (2, 3)):
# we want to be notified and we are online/chat or we don't mind # we want to be notified and we are online/chat or we don't mind
# bugged when away/na/busy # bugged when away/na/busy
notify.popup(event_type, jid, account, msg_type, path_to_image=path, app.notification.popup(
title=event_type, text=txt) event_type, jid, account, msg_type,
icon_name=icon_name, title=event_type, text=txt)
def handle_event_signed_in(self, obj): def handle_event_signed_in(self, obj):
""" """
@ -1278,10 +1280,10 @@ class Interface:
# TODO: we should use another pixmap ;-) # TODO: we should use another pixmap ;-)
txt = _('%s wants to start a voice chat.') % \ txt = _('%s wants to start a voice chat.') % \
app.get_name_from_jid(account, obj.fjid) app.get_name_from_jid(account, obj.fjid)
path = gtkgui_helpers.get_icon_path('gajim-mic_active', 48)
event_type = _('Voice Chat Request') event_type = _('Voice Chat Request')
notify.popup(event_type, obj.fjid, account, 'jingle-incoming', app.notification.popup(
path_to_image=path, title=event_type, text=txt) event_type, obj.fjid, account, 'jingle-incoming',
icon_name='gajim-mic_active', title=event_type, text=txt)
def handle_event_jingle_connected(self, obj): def handle_event_jingle_connected(self, obj):
# ('JINGLE_CONNECTED', account, (peerjid, sid, media)) # ('JINGLE_CONNECTED', account, (peerjid, sid, media))
@ -1652,28 +1654,7 @@ class Interface:
elif type_ in ('printed_chat', 'chat', ''): elif type_ in ('printed_chat', 'chat', ''):
# '' is for log in/out notifications # '' is for log in/out notifications
if type_ != '': ctrl = self.msg_win_mgr.search_control(jid, account, resource)
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)
if not ctrl: if not ctrl:
highest_contact = app.contacts.\ highest_contact = app.contacts.\
@ -1692,33 +1673,20 @@ class Interface:
if not contact: if not contact:
contact = highest_contact contact = highest_contact
ctrl = self.new_chat(contact, account, resource=resource, ctrl = self.new_chat(contact, account, resource=resource)
session=session)
app.last_message_time[account][jid] = 0 # long time ago app.last_message_time[account][jid] = 0 # long time ago
w = ctrl.parent_win w = ctrl.parent_win
elif type_ in ('printed_pm', 'pm'): 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 = self.msg_win_mgr.get_control(fjid, account)
ctrl = event.control
elif type_ == 'pm':
session = event.session
if session and session.control: if not ctrl:
ctrl = session.control
elif not ctrl:
room_jid = jid room_jid = jid
nick = resource nick = resource
gc_contact = app.contacts.get_gc_contact(account, room_jid, gc_contact = app.contacts.get_gc_contact(
nick) account, room_jid, nick)
if gc_contact: if gc_contact:
show = gc_contact.show show = gc_contact.show
else: else:
@ -1727,12 +1695,7 @@ class Interface:
room_jid=room_jid, account=account, name=nick, room_jid=room_jid, account=account, name=nick,
show=show) show=show)
if not session: ctrl = self.new_private_chat(gc_contact, account)
session = app.connections[account].make_new_session(
fjid, None, type_='pm')
self.new_private_chat(gc_contact, account, session=session)
ctrl = session.control
w = ctrl.parent_win w = ctrl.parent_win
elif type_ in ('normal', 'file-request', 'file-request-error', 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 Sebastian Estienne # Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2005-2006 Andrew Sayman <lorien420 AT myrealbox.com> # Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com> # Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
## Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org> # Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2006 Travis Shirk <travis AT pobox.com> # Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org> # Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com> # Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
## Stephan Erb <steve-e AT h3c.de> # Jonathan Schleifer <js-gajim AT webkeks.org>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com> # Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
## Jonathan Schleifer <js-gajim AT webkeks.org> #
## # This file is part of Gajim.
## This file is part of Gajim. #
## # Gajim is free software; you can redistribute it and/or modify
## Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published
## it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only.
## by the Free Software Foundation; version 3 only. #
## # Gajim is distributed in the hope that it will be useful,
## Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of
## but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details.
## GNU General Public License for more details. #
## # You should have received a copy of the GNU General Public License
## You should have received a copy of the GNU General Public License # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
import sys import sys
from gajim.dialogs import PopupNotificationWindow
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gio 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 app
from gajim.common import helpers from gajim.common import helpers
from gajim.common import ged from gajim.common import ged
def get_show_in_roster(event, account, contact, session=None): def get_show_in_roster(event, account, contact, session=None):
""" """
Return True if this event must be shown in roster, else False 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 False
return True return True
def get_show_in_systray(event, account, contact, type_=None): def get_show_in_systray(event, account, contact, type_=None):
""" """
Return True if this event must be shown in systray, else False 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', notify = app.config.get('notify_on_all_muc_messages')
contact.jid, 'notify_on_all_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 # it's not an highlighted message, don't show in systray
return False return False
return app.config.get('trayicon_notification_on_events') 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: class Notification:
""" """
Handle notifications Handle notifications
""" """
def __init__(self): def __init__(self):
app.ged.register_event_handler('notification', ged.GUI2, app.ged.register_event_handler(
self._nec_notification) 'notification', ged.GUI2, self._nec_notification)
def _nec_notification(self, obj): def _nec_notification(self, obj):
if obj.do_popup: if obj.do_popup:
if obj.popup_image: icon_name = self._get_icon_name(obj)
icon_path = gtkgui_helpers.get_icon_path(obj.popup_image, 48) self.popup(obj.popup_event_type, obj.jid, obj.conn.name,
if icon_path: obj.popup_msg_type, icon_name=icon_name,
image_path = icon_path title=obj.popup_title, text=obj.popup_text,
elif obj.popup_image_path: timeout=obj.popup_timeout)
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)
if obj.do_sound: if obj.do_sound:
if obj.sound_file: if obj.sound_file:
@ -145,3 +92,190 @@ class Notification:
helpers.exec_command(obj.command, use_shell=True) helpers.exec_command(obj.command, use_shell=True)
except Exception: except Exception:
pass 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()