Commit 9c7f2b95 authored by Philipp Hörist's avatar Philipp Hörist
Browse files

Rework Groupchat Roster

- Add own module for roster code
- Use avatars with status integrated
- Favor performance over features
parent 1b0e5f89
......@@ -1320,7 +1320,7 @@ def redraw_after_event_removed(self, jid):
if contact:
app.interface.roster.draw_contact(room_jid, self.account)
if groupchat_control:
groupchat_control.draw_contact(nick)
groupchat_control.roster.draw_contact(nick)
if groupchat_control.parent_win:
groupchat_control.parent_win.redraw_tab(groupchat_control)
else:
......
......@@ -282,17 +282,17 @@ def nick(self, new_nick):
def chat(self, nick):
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
if nick in nicks:
self.on_send_pm(nick=nick)
self.send_pm(nick)
else:
raise CommandError(_("Nickname not found"))
@command('msg', raw=True)
@doc(_("Open a private chat window with a specified participant and send "
"him a message"))
def message(self, nick, a_message):
def message(self, nick, message):
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
if nick in nicks:
self.on_send_pm(nick=nick, msg=a_message)
self.send_pm(nick, message)
else:
raise CommandError(_("Nickname not found"))
......
......@@ -128,7 +128,6 @@ class Config:
'custombrowser': [opt_str, DEFAULT_BROWSER],
'custommailapp': [opt_str, DEFAULT_MAILAPP],
'custom_file_manager': [opt_str, DEFAULT_FILE_MANAGER],
'gc-hpaned-position': [opt_int, 430],
'gc_refer_to_nick_char': [opt_str, ',', _('Character to add after nickname when using nickname completion (tab) in group chat.')],
'gc_proposed_nick_char': [opt_str, '_', _('Character to propose to add after desired nickname when nickname is already used in group chat.')],
'msgwin-max-state': [opt_bool, False],
......@@ -289,6 +288,7 @@ class Config:
'muclumbus_api_pref': [opt_str, 'http', _('API Preferences. Possible values: \'http\', \'iq\'')],
'auto_copy': [opt_bool, True, _('Selecting text will copy it to the clipboard')],
'command_system_execute': [opt_bool, False, _('If enabled, Gajim will execute commands (/show, /sh, /execute, /exec).')],
'groupchat_roster_width': [opt_int, 210, _('Width of group chat roster in pixel')],
}, {}) # type: Tuple[Dict[str, List[Any]], Dict[Any, Any]]
__options_per_key = {
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkAccelGroup" id="accelgroup1"/>
<object class="GtkMenu" id="gc_occupants_menu">
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="send_private_message_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Send Private Message</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="send_file_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Send _File</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="group_chat_actions_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Participant Actions</property>
<property name="use_underline">True</property>
<child type="submenu">
<object class="GtkMenu" id="group_chat_actions_menuitem_menu">
<property name="can_focus">False</property>
<child>
<object class="GtkCheckMenuItem" id="voice_checkmenuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Voice</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="moderator_checkmenuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Mo_derator</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="separator5">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="member_checkmenuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Member</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="admin_checkmenuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Admin</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="owner_checkmenuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Owner</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="separator4">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="kick_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Kick</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="ban_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Ban</property>
<property name="use_underline">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkMenuItem" id="invite_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">In_vite to</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="separator6">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="add_to_roster_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Add to Contact List</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="execute_command_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Execute command</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="block_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Block</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="unblock_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Unblock</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="separator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="information_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">_Information</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="history_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_History</property>
<property name="use_underline">True</property>
</object>
</child>
</object>
</interface>
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkTreeStore" id="participant_store">
<columns>
<!-- column-name Avatar -->
<column type="CairoGObjectSurface"/>
<!-- column-name Text -->
<column type="gchararray"/>
<!-- column-name Event -->
<column type="gboolean"/>
<!-- column-name IsContact -->
<column type="gboolean"/>
<!-- column-name GroupOrNickname -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkTreeView" id="roster_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">participant_store</property>
<property name="headers_visible">False</property>
<property name="expander_column">expander</property>
<property name="search_column">1</property>
<signal name="button-press-event" handler="_on_roster_button_press_event" swapped="no"/>
<signal name="focus-out-event" handler="_on_focus_out" swapped="no"/>
<signal name="row-activated" handler="_on_roster_row_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="contact_column">
<property name="sizing">fixed</property>
<property name="fixed_width">210</property>
<property name="title">avatar</property>
<property name="expand">True</property>
<child>
<object class="GtkCellRendererPixbuf" id="avatar_renderer">
<property name="width">40</property>
<property name="xalign">0</property>
</object>
<attributes>
<attribute name="visible">3</attribute>
<attribute name="surface">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="text_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="markup">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="event_column">
<property name="sizing">fixed</property>
<property name="fixed_width">35</property>
<property name="title">event</property>
<child>
<object class="GtkCellRendererPixbuf" id="icon">
<property name="xalign">0</property>
<property name="icon_name">gajim-event</property>
</object>
<attributes>
<attribute name="visible">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="expander">
<property name="visible">False</property>
<property name="sizing">fixed</property>
<property name="title">expander</property>
</object>
</child>
</object>
</interface>
......@@ -141,6 +141,10 @@ .start-chat-row { padding: 10px 20px 10px 10px; }
.start-chat-row:not(.activatable) label { color: @insensitive_fg_color }
.start-chat-row:focus { outline: none; }
/* GroupChatRoster */
.groupchat-roster { border-left: 1px solid;
border-color: @borders; }
.groupchat-roster treeview { padding-left: 4px; }
/* GroupchatConfig */
#GroupchatConfig > box > buttonbox { margin: 0px 12px 12px 12px; }
......
This diff is collapsed.
# 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/>.
from typing import Optional
import locale
from enum import IntEnum
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import GObject
from nbxmpp.const import Role
from nbxmpp.const import Affiliation
from gajim.common import app
from gajim.common import ged
from gajim.common.helpers import get_uf_role
from gajim.common.helpers import get_uf_affiliation
from gajim.common.helpers import jid_is_blocked
from gajim.common.helpers import event_filter
from gajim.common.const import AvatarSize
from gajim.common.const import StyleAttr
from gajim.gui_menu_builder import get_groupchat_roster_menu
from gajim.gtk.tooltips import GCTooltip
from gajim.gtk.util import get_builder
AffiliationRoleSortOrder = {
'owner': 0,
'admin': 1,
'moderator': 2,
'participant': 3,
'visitor': 4
}
class Column(IntEnum):
AVATAR = 0
TEXT = 1
EVENT = 2
IS_CONTACT = 3
NICK_OR_GROUP = 4
class GroupchatRoster(Gtk.ScrolledWindow):
__gsignals__ = {
'row-activated': (
GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
None, # return value
(str, )) # arguments
}
def __init__(self, account, room_jid, control):
Gtk.ScrolledWindow.__init__(self)
self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self.get_style_context().add_class('groupchat-roster')
self._account = account
self.room_jid = room_jid
self._control = control
self._control_id = control.control_id
self._show_roles = True
self._handler_ids = {}
self._tooltip = GCTooltip()
self._ui = get_builder('groupchat_roster.ui')
self._ui.roster_treeview.set_model(None)
self.add(self._ui.roster_treeview)
# Holds the Gtk.TreeRowReference for each contact
self._contact_refs = {}
# Holds the Gtk.TreeRowReference for each group
self._group_refs = {}
self._store = self._ui.participant_store
self._store.set_sort_func(Column.TEXT, self._tree_compare_iters)
self._roster = self._ui.roster_treeview
self._roster.set_search_equal_func(self._search_func)
self._ui.contact_column.set_fixed_width(
app.config.get('groupchat_roster_width'))
self._ui.contact_column.set_cell_data_func(self._ui.text_renderer,
self._text_cell_data_func)
self.connect('destroy', self._on_destroy)
self._ui.connect_signals(self)
self._event_handlers = [
('theme-update', ged.GUI2, self._on_theme_update),
('update-gc-avatar', ged.GUI1, self._on_avatar_update),
]
for handler in self._event_handlers:
app.ged.register_event_handler(*handler)
@staticmethod
def _on_focus_out(treeview, _param):
treeview.get_selection().unselect_all()
def set_model(self):
self._roster.set_model(self._store)
def set_show_roles(self, enabled):
self._show_roles = enabled
def enable_tooltips(self):
if self._roster.get_tooltip_window():
return
self._roster.set_has_tooltip(True)
id_ = self._roster.connect('query-tooltip', self._query_tooltip)
self._handler_ids[id_] = self._roster
def _query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip):
try:
row = self._roster.get_path_at_pos(x_pos, y_pos)[0]
except TypeError:
self._tooltip.clear_tooltip()
return False
if not row:
self._tooltip.clear_tooltip()
return False
iter_ = None
try:
iter_ = self._store.get_iter(row)
except Exception:
self._tooltip.clear_tooltip()
return False
if not self._store[iter_][Column.IS_CONTACT]:
self._tooltip.clear_tooltip()
return False
nickname = self._store[iter_][Column.NICK_OR_GROUP]
contact = app.contacts.get_gc_contact(self._account,
self.room_jid,
nickname)
if contact is None:
self._tooltip.clear_tooltip()
return False
value, widget = self._tooltip.get_tooltip(contact)
tooltip.set_custom(widget)
return value
@staticmethod
def _search_func(model, _column, search_text, iter_):
return search_text.lower() not in model[iter_][1].lower()
def _get_group_iter(self, group_name: str) -> Optional[Gtk.TreeIter]:
try:
ref = self._group_refs[group_name]
except KeyError:
return None
path = ref.get_path()
if path is None:
return None
return self._store.get_iter(path)
def _get_contact_iter(self, nick: str) -> Optional[Gtk.TreeIter]:
try:
ref = self._contact_refs[nick]
except KeyError:
return None
path = ref.get_path()
if path is None:
return None
return self._store.get_iter(path)
def add_contact(self, nick):
contact = app.contacts.get_gc_contact(self._account,
self.room_jid,
nick)
group_name, group_text = self._get_group_from_contact(contact)
# Create Group
group_iter = self._get_group_iter(group_name)
role_path = None
if not group_iter:
group_iter = self._store.append(
None, (None, group_text, None, False, group_name))
role_path = self._store.get_path(group_iter)
group_ref = Gtk.TreeRowReference(self._store, role_path)
self._group_refs[group_name] = group_ref
# Avatar
surface = app.interface.get_avatar(contact,
AvatarSize.ROSTER,
self.get_scale_factor(),
contact.show.value)
iter_ = self._store.append(group_iter,
(surface, nick, None, True, nick))
self._contact_refs[nick] = Gtk.TreeRowReference(
self._store, self._store.get_path(iter_))
self.draw_groups()
self.draw_contact(nick)
if (role_path is not None and
self._roster.get_model() is not None):
self._roster.expand_row(role_path, False)
def remove_contact(self, nick):
"""
Remove a user
"""
iter_ = self._get_contact_iter(nick)
if not iter_:
return
group_iter = self._store.iter_parent(iter_)
if group_iter is None:
raise ValueError('Trying to remove non-child')
self._store.remove(iter_)
del self._contact_refs[nick]
if not self._store.iter_has_child(group_iter):
group = self._store[group_iter][Column.NICK_OR_GROUP]
del self._group_refs[group]
self._store.remove(group_iter)
@staticmethod
def _get_group_from_contact(contact):
if contact.affiliation in (Affiliation.OWNER, Affiliation.ADMIN):
return contact.affiliation.value, get_uf_affiliation(
contact.affiliation, plural=True)
return contact.role.value, get_uf_role(contact.role, plural=True)
@staticmethod
def _text_cell_data_func(_column, renderer, model, iter_, _user_data):
has_parent = bool(model.iter_parent(iter_))
style = 'contact' if has_parent else 'group'
bgcolor = app.css_config.get_value('.gajim-%s-row' % style,
StyleAttr.BACKGROUND)
renderer.set_property('cell-background', bgcolor)
color = app.css_config.get_value('.gajim-%s-row' % style,
StyleAttr.COLOR)
renderer.set_property('foreground', color)
desc = app.css_config.get_font('.gajim-%s-row' % style)
renderer.set_property('font-desc', desc)
if not has_parent:
renderer.set_property('weight', 600)
renderer.set_property('height', 32)
elif app.config.get('show_status_msgs_in_roster'):
renderer.set_property('height', 38)
else:
renderer.set_property('height', 32)