Daniel Brötzmann pushed to branch mainwindow at gajim / gajim
Commits: ec1afbee by wurstsalat at 2021-03-02T20:47:43+01:00 Workspaces: Fix menu, store moved chats
- - - - - 0fb26922 by wurstsalat at 2021-03-02T20:56:00+01:00 AccountPage: Add basic roster
- - - - -
5 changed files:
- + gajim/data/gui/roster.ui - gajim/gtk/account_page.py - gajim/gtk/main.py - + gajim/gtk/roster.py - gajim/gui_menu_builder.py
Changes:
===================================== gajim/data/gui/roster.ui ===================================== @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.38.2 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkTreeStore" id="contact_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">contact_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="ypad">3</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>
===================================== gajim/gtk/account_page.py ===================================== @@ -17,6 +17,7 @@ from gajim.common import app from gajim.common.i18n import _
+from .roster import Roster from .status_selector import StatusSelector from .util import open_window
@@ -46,7 +47,12 @@ def __init__(self, account): box.add(settings_button) box.add(self._status_selector)
- self.add(box) + self._roster = Roster(account) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + hbox.add(self._roster) + hbox.add(box) + + self.add(hbox) self.show_all() self.update()
@@ -59,3 +65,6 @@ def update(self): self._account, 'account_label') self._account_label.set_text(account_label) self._status_selector.update() + + def process_event(self, event): + self._roster.process_event(event)
===================================== gajim/gtk/main.py ===================================== @@ -54,20 +54,22 @@ def __init__(self):
self.add(self._ui.main_grid)
+ surface = load_icon('org.gajim.Gajim', self, 40) + self._ui.app_image.set_from_surface(surface) + self._chat_stack = ChatStack() + self._ui.right_grid.add(self._chat_stack) + self._chat_list_stack = ChatListStack(self, self._ui, self._chat_stack) self._ui.chat_list_scrolled.add(self._chat_list_stack)
- self._account_side_bar = AccountSideBar() self._workspace_side_bar = WorkspaceSideBar(self._chat_list_stack) - - surface = load_icon('org.gajim.Gajim', self, 40) - self._ui.app_image.set_from_surface(surface) self._ui.workspace_scrolled.add(self._workspace_side_bar)
+ self._account_side_bar = AccountSideBar() self._ui.account_box.add(self._account_side_bar)
- self._ui.right_grid.add(self._chat_stack) + self._account_pages = {}
workspace_menu = Gio.Menu() for action, label in WORKSPACE_MENU_DICT.items(): @@ -419,6 +421,8 @@ def move_chat_to_workspace(self, new_workspace_id, account, jid): current_chatlist.remove_chat(account, jid) new_chatlist = self._chat_list_stack.get_chatlist(new_workspace_id) new_chatlist.add_chat(account, jid, type_) + self._chat_list_stack.store_open_chats(current_chatlist.workspace_id) + self._chat_list_stack.store_open_chats(new_workspace_id)
def _remove_chat(self, _action, param): workspace_id, account, jid = param.unpack() @@ -445,8 +449,9 @@ def get_active_control(self):
def _add_accounts(self): for account in list(app.connections.keys()): - self._ui.main_stack.add_named( - AccountPage(account), account) + account_page = AccountPage(account) + self._account_pages[account] = account_page + self._ui.main_stack.add_named(account_page, account)
def get_controls(self, account=None): return self._chat_stack.get_controls(account) @@ -484,6 +489,9 @@ def _on_event(self, event): if event.name == 'update-roster-avatar': return
+ for page in self._account_pages.values(): + page.process_event(event) + if not self.chat_exists(event.account, event.jid): if event.name == 'message-received': if event.properties.is_muc_pm:
===================================== gajim/gtk/roster.py ===================================== @@ -0,0 +1,247 @@ +import logging +from typing import Optional +from enum import IntEnum + +from gi.repository import GLib +from gi.repository import Gtk +from gi.repository import GObject + +from gajim.common import app +from gajim.common.const import AvatarSize +from gajim.common.i18n import _ + +from .util import get_builder + +log = logging.getLogger('gajim.gui.roster') + + +HANDLED_EVENTS = [ + 'presence-received', +] + + +class Column(IntEnum): + AVATAR = 0 + TEXT = 1 + EVENT = 2 + IS_CONTACT = 3 + NICK_OR_GROUP = 4 + + +class Roster(Gtk.ScrolledWindow): + + __gsignals__ = { + 'row-activated': ( + GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, + None, # return value + (str, )) # arguments + } + + def __init__(self, account): + Gtk.ScrolledWindow.__init__(self) + self.set_size_request(300, -1) + + self._account = account + + self._handler_ids = {} + + self._ui = get_builder('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.contact_store + + self._roster = self._ui.roster_treeview + + self.connect('destroy', self._on_destroy) + self._ui.connect_signals(self) + + self._initial_draw() + + def _on_roster_row_activated(self, _treeview, path, _column): + iter_ = self._store.get_iter(path) + if self._store.iter_parent(iter_) is None: + # This is a group row + return + + nick = self._store[iter_][Column.NICK_OR_GROUP] + self.emit('row-activated', nick) + + def _on_roster_button_press_event(self, treeview, event): + if event.button not in (2, 3): + return + + pos = treeview.get_path_at_pos(int(event.x), int(event.y)) + if pos is None: + return + + path, _, _, _ = pos + iter_ = self._store.get_iter(path) + if self._store.iter_parent(iter_) is None: + # Group row + return + + nick = self._store[iter_][Column.NICK_OR_GROUP] + + if event.button == 3: # right click + self._show_contact_menu(nick) + + if event.button == 2: # middle click + self.emit('row-activated', nick) + + @staticmethod + def _on_focus_out(treeview, _param): + treeview.get_selection().unselect_all() + + def _show_contact_menu(self, nick): + pass + + def _on_destroy(self, _roster): + for id_ in list(self._handler_ids.keys()): + if self._handler_ids[id_].handler_is_connected(id_): + self._handler_ids[id_].disconnect(id_) + del self._handler_ids[id_] + + self._contact_refs = {} + self._group_refs = {} + self._roster.set_model(None) + self._roster = None + self._store.clear() + self._store = None + # self._tooltip.destroy() + # self._tooltip = None + + def set_model(self): + self._roster.set_model(self._store) + + def _initial_draw(self): + client = app.get_client(self._account) + for contact in client.get_module('Roster').iter_contacts(): + contact.connect('presence-update', self._on_presence_update) + contact.connect('avatar-update', self._on_avatar_update) + self._add_contact(contact) + + self.set_model() + self._roster.expand_all() + + def _on_presence_update(self, contact, _signal_name): + self.draw_avatar(contact) + + def _on_avatar_update(self, contact, _signal_name): + self.draw_avatar(contact) + + def _add_contact(self, contact): + main_group = _('Contacts') + + if contact.groups: + for group_name in contact.groups: + group_iter = self._get_group_iter(group_name) + if not group_iter: + group_iter = self._store.append( + None, (None, group_name, None, False, group_name)) + group_path = self._store.get_path(group_iter) + group_ref = Gtk.TreeRowReference(self._store, group_path) + self._group_refs[group_name] = group_ref + else: + group_iter = self._get_group_iter(main_group) + if not group_iter: + group_iter = self._store.append( + None, (None, main_group, None, False, main_group)) + group_path = self._store.get_path(group_iter) + group_ref = Gtk.TreeRowReference(self._store, group_path) + self._group_refs[main_group] = group_ref + + # Avatar + surface = contact.get_avatar(AvatarSize.CHAT, self.get_scale_factor()) + + iter_ = self._store.append( + group_iter, (surface, contact.name, None, True, contact.name)) + self._contact_refs[contact.jid] = Gtk.TreeRowReference( + self._store, self._store.get_path(iter_)) + + self.draw_groups() + self.draw_contact(contact.jid) + + if (group_path is not None and + self._roster.get_model() is not None): + self._roster.expand_row(group_path, False) + + def draw_groups(self): + for group in self._group_refs: + self.draw_group(group) + + def draw_group(self, group): + group_iter = self._get_group_iter(group) + if not group_iter: + return + + self._store[group_iter][Column.TEXT] = group + + def draw_contacts(self): + for jid in self._contact_refs: + self.draw_contact(jid) + + def draw_contact(self, jid): + iter_ = self._get_contact_iter(jid) + if not iter_: + return + + client = app.get_client(self._account) + contact = client.get_module('Contacts').get_contact(jid) + + self.draw_avatar(contact) + self._store[iter_][Column.TEXT] = GLib.markup_escape_text(contact.name) + + def draw_avatar(self, contact): + iter_ = self._get_contact_iter(contact.jid) + if iter_ is None: + return + + surface = contact.get_avatar( + AvatarSize.ROSTER, self.get_scale_factor()) + self._store[iter_][Column.AVATAR] = surface + + 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, jid: str) -> Optional[Gtk.TreeIter]: + try: + ref = self._contact_refs[jid] + except KeyError: + return None + + path = ref.get_path() + if path is None: + return None + + return self._store.get_iter(path) + + def clear(self): + self._contact_refs = {} + self._group_refs = {} + self._store.clear() + + def _on_presence_received(self, event): + pass + + def process_event(self, event): + if event.name not in HANDLED_EVENTS: + return + + if event.name == 'presence-received': + self._on_presence_received(event) + else: + log.warning('Unhandled Event: %s', event.name)
===================================== gajim/gui_menu_builder.py ===================================== @@ -809,12 +809,13 @@ def get_chat_list_row_menu(workspace_id, account, jid, pinned): action, label = item action = f'win.{action}' menuitem = Gio.MenuItem.new(label, action) - variant_list = GLib.Variant('as', [workspace_id, account, jid]) + variant_list = GLib.Variant( + 'as', [workspace_id, account, str(jid)]) menuitem.set_action_and_target_value(action, variant_list) menu.append_item(menuitem) else: # This is a submenu - submenu = build_workspaces_submenu(workspace_id, account, jid) + submenu = build_workspaces_submenu(workspace_id, account, str(jid)) menu.append_submenu(item[0], submenu)
return menu
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/d48d1772464ba6a1bea791bd0d8903a8...