Commit b9b9dae6 authored by Philipp Hörist's avatar Philipp Hörist

Refactor UserActivity

- Use IconTheme for mood icons
- Simplify modules because nbxmpp handles more stuff
parent be95b040
Pipeline #3007 passed with stages
in 3 minutes and 21 seconds
......@@ -60,6 +60,8 @@ from gajim.gtk.util import get_icon_name
from gajim.gtk.util import get_cursor
from gajim.gtk.util import ensure_proper_control
from gajim.gtk.util import format_mood
from gajim.gtk.util import format_activity
from gajim.gtk.util import get_activity_icon_name
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.framework import CommandHost # pylint: disable=unused-import
......@@ -131,7 +133,6 @@ class ChatControl(ChatControlBase):
self.update_toolbar()
self._pep_images = {}
self._pep_images['activity'] = self.xml.get_object('activity_image')
self._pep_images['tune'] = self.xml.get_object('tune_image')
self._pep_images['geoloc'] = self.xml.get_object('location_image')
self.update_all_pep_types()
......@@ -233,6 +234,8 @@ class ChatControl(ChatControlBase):
self._on_nickname_received)
app.ged.register_event_handler('mood-received', ged.GUI1,
self._on_mood_received)
app.ged.register_event_handler('activity-received', ged.GUI1,
self._on_activity_received)
if self.TYPE_ID == message_control.TYPE_CHAT:
# Dont connect this when PrivateChatControl is used
app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
......@@ -416,6 +419,7 @@ class ChatControl(ChatControlBase):
for pep_type in self._pep_images:
self.update_pep(pep_type)
self._update_pep(PEPEventType.MOOD)
self._update_pep(PEPEventType.ACTIVITY)
def update_pep(self, pep_type):
if isinstance(self.contact, GC_Contact):
......@@ -453,6 +457,9 @@ class ChatControl(ChatControlBase):
if type_ == PEPEventType.MOOD:
icon = 'mood-%s' % data.mood
formated_text = format_mood(*data)
elif type_ == PEPEventType.ACTIVITY:
icon = get_activity_icon_name(data.activity, data.subactivity)
formated_text = format_activity(*data)
image.set_from_icon_name(icon, Gtk.IconSize.MENU)
image.set_tooltip_markup(formated_text)
......@@ -461,11 +468,17 @@ class ChatControl(ChatControlBase):
def _get_pep_widget(self, type_):
if type_ == PEPEventType.MOOD:
return self.xml.get_object('mood_image')
if type_ == PEPEventType.ACTIVITY:
return self.xml.get_object('activity_image')
@ensure_proper_control
def _on_mood_received(self, _event):
self._update_pep(PEPEventType.MOOD)
@ensure_proper_control
def _on_activity_received(self, _event):
self._update_pep(PEPEventType.ACTIVITY)
@ensure_proper_control
def _on_nickname_received(self, _event):
self.update_ui()
......@@ -1086,6 +1099,8 @@ class ChatControl(ChatControlBase):
self._on_nickname_received)
app.ged.remove_event_handler('mood-received', ged.GUI1,
self._on_mood_received)
app.ged.remove_event_handler('activity-received', ged.GUI1,
self._on_activity_received)
if self.TYPE_ID == message_control.TYPE_CHAT:
app.ged.remove_event_handler('update-roster-avatar', ged.GUI1,
self._nec_update_avatar)
......
......@@ -93,8 +93,6 @@ class Config:
'trayicon': [opt_str, 'always', _("When to show notification area icon. Can be 'never', 'on_event', 'always'."), False],
'allow_hide_roster': [opt_bool, False, _("Allow to hide the roster window even if the tray icon is not shown."), False],
'iconset': [opt_str, DEFAULT_ICONSET, '', True],
'mood_iconset': [opt_str, DEFAULT_MOOD_ICONSET, '', True],
'activity_iconset': [opt_str, DEFAULT_ACTIVITY_ICONSET, '', True],
'use_transports_iconsets': [opt_bool, True, '', True],
'notif_signin_color': [opt_color, '#32CD32', _('Contact signed in notification color.')], # limegreen
'notif_signout_color': [opt_color, '#FF0000', _('Contact signout notification color')], # red
......
......@@ -221,8 +221,6 @@ class ConfigPaths:
'emoticons', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
('MY_ICONSETS',
'iconsets', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
('MY_ACTIVITY_ICONSETS',
'activities', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
# Cache paths
('CACHE_DB', 'cache.db', PathLocation.CACHE, PathType.FILE),
......
......@@ -1098,13 +1098,6 @@ def get_current_show(account):
status = app.connections[account].connected
return app.SHOW_LIST[status]
def get_activity_iconset_path(iconset):
if os.path.isdir(os.path.join(configpaths.get('DATA'), 'activities', iconset)):
return os.path.join(configpaths.get('DATA'), 'activities', iconset)
if os.path.isdir(os.path.join(configpaths.get('MY_ACTIVITY_ICONSETS'),
iconset)):
return os.path.join(configpaths.get('MY_ACTIVITY_ICONSETS'), iconset)
def prepare_and_validate_gpg_keyID(account, jid, keyID):
"""
Return an eight char long keyID that can be used with for GPG encryption
......
......@@ -35,7 +35,9 @@ ZEROCONF_MODULES = ['iq',
_imported_modules = [] # type: List[tuple]
_modules = {} # type: Dict[str, Dict[str, Any]]
_store_publish_modules = [
'UserMood'] # type: List[str]
'UserMood',
'UserActivity',
] # type: List[str]
for file in Path(__file__).parent.iterdir():
if file.stem == '__init__':
......
......@@ -59,7 +59,7 @@ class Message:
nbxmpp.NS_IBB])
def _message_received(self, _con, stanza, properties):
if properties.is_mam_message or properties.is_pubsub_event:
if properties.is_mam_message or properties.is_pubsub:
return
# Check if a child of the message contains any
# namespaces that we handle in other modules.
......
......@@ -14,92 +14,62 @@
# XEP-0108: User Activity
from typing import Any
from typing import Tuple
import logging
import nbxmpp
from gi.repository import GLib
from gajim.common.const import PEPEventType, ACTIVITIES
from gajim.common.exceptions import StanzaMalformed
from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
from gajim.common.modules.util import store_publish
from gajim.common.const import PEPEventType
log = logging.getLogger('gajim.c.m.user_activity')
class UserActivityData(AbstractPEPData):
type_ = PEPEventType.ACTIVITY
def as_markup_text(self):
pep = self.data
activity = pep['activity']
subactivity = pep['subactivity'] if 'subactivity' in pep else None
text = pep['text'] if 'text' in pep else None
class UserActivity(BaseModule):
if activity in ACTIVITIES:
# Translate standard activities
if subactivity in ACTIVITIES[activity]:
subactivity = ACTIVITIES[activity][subactivity]
activity = ACTIVITIES[activity]['category']
_nbxmpp_extends = 'Activity'
_nbxmpp_methods = [
'set_activity',
]
markuptext = '<b>' + GLib.markup_escape_text(activity)
if subactivity:
markuptext += ': ' + GLib.markup_escape_text(subactivity)
markuptext += '</b>'
if text:
markuptext += ' (%s)' % GLib.markup_escape_text(text)
return markuptext
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._activity_received)
@event_node(nbxmpp.NS_ACTIVITY)
def _activity_received(self, _con, _stanza, properties):
data = properties.pubsub_event.data
for contact in app.contacts.get_contacts(self._account,
str(properties.jid)):
if data.activity is not None:
contact.pep[PEPEventType.ACTIVITY] = data
else:
contact.pep.pop(PEPEventType.ACTIVITY, None)
class UserActivity(AbstractPEPModule):
if properties.is_self_message:
if data.activity is not None:
self._con.pep[PEPEventType.ACTIVITY] = data
else:
self._con.pep.pop(PEPEventType.ACTIVITY, None)
name = 'activity'
namespace = nbxmpp.NS_ACTIVITY
pep_class = UserActivityData
store_publish = True
_log = log
app.nec.push_incoming_event(
NetworkEvent('activity-received',
account=self._account,
jid=properties.jid.getBare(),
activity=data,
is_self_message=properties.is_self_message))
def _extract_info(self, item):
activity_dict = {}
activity_tag = item.getTag('activity', namespace=self.namespace)
if activity_tag is None:
raise StanzaMalformed('No activity node')
@store_publish
def set_activity(self, activity):
log.info('Send %s', activity)
self._nbxmpp('Activity').set_activity(activity)
if not activity_tag.getChildren():
return None
for child in activity_tag.getChildren():
name = child.getName().strip()
data = child.getData().strip()
if name == 'text':
activity_dict['text'] = data
else:
activity_dict['activity'] = name
for subactivity in child.getChildren():
subactivity_name = subactivity.getName().strip()
activity_dict['subactivity'] = subactivity_name
if 'activity' not in activity_dict:
raise StanzaMalformed('No activity value found')
return activity_dict
def _build_node(self, data):
item = nbxmpp.Node('activity', {'xmlns': self.namespace})
if data is None:
return item
activity, subactivity, message = data
if not activity:
return item
i = item.addChild(activity)
if subactivity:
i.addChild(subactivity)
if message:
i = item.addChild('text')
i.addData(message)
return item
def get_instance(*args, **kwargs):
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserActivity, str]:
return UserActivity(*args, **kwargs), 'UserActivity'
......@@ -37,7 +37,6 @@ from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gajim import gtkgui_helpers
from gajim import vcard
from gajim import dataforms_widget
......@@ -58,6 +57,7 @@ from gajim.gtk.add_contact import AddNewContactWindow
from gajim.gtk.util import get_icon_name
from gajim.gtk.util import resize_window
from gajim.gtk.util import get_builder
from gajim.gtk.util import get_activity_icon_name
log = logging.getLogger('gajim.dialogs')
......@@ -369,9 +369,9 @@ class ChangeActivityDialog:
group = None
for category in ACTIVITIES:
icon_name = get_activity_icon_name(category)
item = self.xml.get_object(category + '_image')
item.set_from_pixbuf(
gtkgui_helpers.load_activity_icon(category).get_pixbuf())
item.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
item.set_tooltip_text(ACTIVITIES[category]['category'])
vbox = self.xml.get_object(category + '_vbox')
......@@ -386,7 +386,7 @@ class ChangeActivityDialog:
else:
rbtns[act] = group = Gtk.RadioButton()
icon = gtkgui_helpers.load_activity_icon(category, self.activity)
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
hbox = Gtk.HBox(homogeneous=False, spacing=5)
hbox.pack_start(icon, False, False, 0)
lbl = Gtk.Label(
......@@ -412,7 +412,8 @@ class ChangeActivityDialog:
else:
rbtns[act] = group = Gtk.RadioButton()
icon = gtkgui_helpers.load_activity_icon(category, activity)
icon_name = get_activity_icon_name(category, activity)
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
label = Gtk.Label(label=ACTIVITIES[category][activity])
hbox = Gtk.HBox(homogeneous=False, spacing=5)
hbox.pack_start(icon, False, False, 0)
......@@ -681,12 +682,12 @@ class ChangeStatusMessageDialog(TimeoutDialog):
ACTIVITIES:
if 'subactivity' in self.pep_dict and self.pep_dict['subactivity'] \
in ACTIVITIES[self.pep_dict['activity']]:
img.set_from_pixbuf(gtkgui_helpers.load_activity_icon(
self.pep_dict['activity'], self.pep_dict['subactivity']).\
get_pixbuf())
icon_name = get_activity_icon_name(self.pep_dict['activity'],
self.pep_dict['subactivity'])
img.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
else:
img.set_from_pixbuf(gtkgui_helpers.load_activity_icon(
self.pep_dict['activity']).get_pixbuf())
icon_name = get_activity_icon_name(self.pep_dict['activity'])
img.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
if self.pep_dict['activity_text']:
label.set_text(self.pep_dict['activity_text'])
else:
......
......@@ -43,6 +43,8 @@ from gajim.common.i18n import _
from gajim.gtk.util import get_builder
from gajim.gtk.util import get_icon_name
from gajim.gtk.util import format_mood
from gajim.gtk.util import format_activity
log = logging.getLogger('gajim.gtk.tooltips')
......@@ -479,8 +481,8 @@ class RosterTooltip(StatusTable):
self._ui.mood.show()
self._ui.mood_label.show()
if 'activity' in contact.pep:
activity = contact.pep['activity'].as_markup_text()
if PEPEventType.ACTIVITY in contact.pep:
activity = format_activity(*contact.pep[PEPEventType.ACTIVITY])
self._ui.activity.set_markup(activity)
self._ui.activity.show()
self._ui.activity_label.show()
......
......@@ -37,6 +37,7 @@ from gajim.common import configpaths
from gajim.common import i18n
from gajim.common.i18n import _
from gajim.common.const import MOODS
from gajim.common.const import ACTIVITIES
from gajim.gtk.const import GajimIconSet
......@@ -521,3 +522,27 @@ def format_mood(mood, text):
if text is not None:
markuptext += ' (%s)' % GLib.markup_escape_text(text)
return markuptext
def format_activity(activity, subactivity, text):
if activity is None:
return
if subactivity in ACTIVITIES[activity]:
subactivity = ACTIVITIES[activity][subactivity]
activity = ACTIVITIES[activity]['category']
markuptext = '<b>' + GLib.markup_escape_text(activity)
if subactivity:
markuptext += ': ' + GLib.markup_escape_text(subactivity)
markuptext += '</b>'
if text:
markuptext += ' (%s)' % GLib.markup_escape_text(text)
return markuptext
def get_activity_icon_name(activity, subactivity=None):
icon_name = 'activity-%s' % activity.replace('_', '-')
if subactivity is not None:
icon_name += '-%s' % subactivity.replace('_', '-')
return icon_name
......@@ -41,10 +41,7 @@ except Exception:
from gajim.common.i18n import _
from gajim.common import app
from gajim.common import helpers
from gajim.common.const import PEPEventType
from gajim.common.const import ACTIVITIES
from gajim.common.const import MOODS
HAS_PYWIN32 = True
if os.name == 'nt':
......@@ -267,72 +264,15 @@ def create_list_multi(value_list, selected_values=None):
treeview.show_all()
return treeview
def load_activity_icon(category, activity=None):
"""
Load an icon from the activity iconset in 16x16
"""
iconset = app.config.get('activity_iconset')
path = os.path.join(helpers.get_activity_iconset_path(iconset),
category, '')
if activity is None:
activity = 'category'
icon_list = _load_icon_list([activity], path)
return icon_list[activity]
def get_pep_icon(pep_class):
if pep_class == PEPEventType.TUNE:
return 'audio-x-generic'
if pep_class == PEPEventType.ACTIVITY:
pep_ = pep_class.data
activity = pep_['activity']
has_known_activity = activity in ACTIVITIES
has_known_subactivity = (has_known_activity and
'subactivity' in pep_ and
pep_['subactivity'] in ACTIVITIES[activity])
if has_known_activity:
if has_known_subactivity:
subactivity = pep_['subactivity']
return load_activity_icon(activity, subactivity).get_pixbuf()
return load_activity_icon(activity).get_pixbuf()
return load_activity_icon('unknown').get_pixbuf()
if pep_class == PEPEventType.LOCATION:
return 'applications-internet'
return None
def _load_icon_list(icons_list, path, pixbuf2=None):
"""
Load icons in icons_list from the given path, and add pixbuf2 on top left of
each static images
"""
imgs = {}
for icon in icons_list:
# try to open a pixfile with the correct method
icon_file = icon.replace(' ', '_')
files = []
files.append(path + icon_file + '.gif')
files.append(path + icon_file + '.png')
image = Gtk.Image()
image.show()
imgs[icon] = image
for file_ in files: # loop seeking for either gif or png
if os.path.exists(file_):
image.set_from_file(file_)
if pixbuf2 and image.get_storage_type() == Gtk.ImageType.PIXBUF:
# add pixbuf2 on top-left corner of image
pixbuf1 = image.get_pixbuf()
pixbuf2.composite(pixbuf1, 0, 0,
pixbuf2.get_property('width'),
pixbuf2.get_property('height'), 0, 0, 1.0, 1.0,
GdkPixbuf.InterpType.NEAREST, 255)
image.set_from_pixbuf(pixbuf1)
break
return imgs
def label_set_autowrap(widget):
"""
Make labels automatically re-wrap if their containers are resized.
......
......@@ -37,13 +37,13 @@ from enum import IntEnum, unique
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import Pango
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from nbxmpp.protocol import NS_FILE, NS_ROSTERX, NS_CONFERENCE
from nbxmpp.structs import MoodData
from nbxmpp.structs import ActivityData
from gajim import dialogs
from gajim import vcard
......@@ -87,6 +87,7 @@ from gajim.gtk.util import move_window
from gajim.gtk.util import get_metacontact_surface
from gajim.gtk.util import get_builder
from gajim.gtk.util import set_urgency_hint
from gajim.gtk.util import get_activity_icon_name
log = logging.getLogger('gajim.roster')
......@@ -1049,10 +1050,11 @@ class RosterWindow:
else:
self.model[child_iter][Column.MOOD_PIXBUF] = None
if app.config.get('show_activity_in_roster') and 'activity' in \
pep_dict:
self.model[child_iter][Column.ACTIVITY_PIXBUF] = \
gtkgui_helpers.get_pep_icon(pep_dict['activity'])
if app.config.get('show_activity_in_roster') and PEPEventType.ACTIVITY in pep_dict:
activity = pep_dict[PEPEventType.ACTIVITY].activity
subactivity = pep_dict[PEPEventType.ACTIVITY].subactivity
icon_name = get_activity_icon_name(activity, subactivity)
self.model[child_iter][Column.ACTIVITY_PIXBUF] = icon_name
else:
self.model[child_iter][Column.ACTIVITY_PIXBUF] = None
......@@ -1314,7 +1316,7 @@ class RosterWindow:
if pep_type == PEPEventType.MOOD:
return app.config.get('show_mood_in_roster')
if pep_type == 'activity':
if pep_type == PEPEventType.ACTIVITY:
return app.config.get('show_activity_in_roster')
if pep_type == 'tune':
......@@ -1329,6 +1331,7 @@ class RosterWindow:
for pep_type in self._pep_type_to_model_column:
self.draw_pep(jid, account, pep_type, contact=contact)
self._draw_pep(account, jid, PEPEventType.MOOD)
self._draw_pep(account, jid, PEPEventType.ACTIVITY)
def draw_pep(self, jid, account, pep_type, contact=None):
if pep_type not in self._pep_type_to_model_column:
......@@ -1366,6 +1369,10 @@ class RosterWindow:
column = Column.MOOD_PIXBUF
if data is not None:
icon = 'mood-%s' % data.mood
elif type_ == PEPEventType.ACTIVITY:
column = Column.ACTIVITY_PIXBUF
if data is not None:
icon = get_activity_icon_name(data.activity, data.subactivity)
for child_iter in iters:
self.model[child_iter][column] = icon
......@@ -2094,15 +2101,14 @@ class RosterWindow:
def send_pep(self, account, pep_dict):
connection = app.connections[account]
if 'activity' in pep_dict:
activity = pep_dict['activity']
subactivity = pep_dict.get('subactivity', None)
activity_text = pep_dict.get('activity_text', None)
connection.get_module('UserActivity').send(
(activity, subactivity, activity_text))
connection.get_module('UserActivity').set_activity(ActivityData(
activity, subactivity, activity_text))
else:
connection.get_module('UserActivity').send(None)
connection.get_module('UserActivity').set_activity(None)
if 'mood' in pep_dict:
mood = pep_dict['mood']
......@@ -2630,8 +2636,7 @@ class RosterWindow:
self.remove_contact(jid, obj.conn.name, backend=True)
def _nec_pep_received(self, obj):
if obj.user_pep.type_ not in (PEPEventType.ACTIVITY,
PEPEventType.TUNE,