Daniel Brötzmann pushed to branch mainwindow at gajim / gajim

Commits:

5 changed files:

Changes:

  • gajim/data/gui/roster.ui
    1
    +<?xml version="1.0" encoding="UTF-8"?>
    
    2
    +<!-- Generated with glade 3.38.2 -->
    
    3
    +<interface>
    
    4
    +  <requires lib="gtk+" version="3.20"/>
    
    5
    +  <object class="GtkTreeStore" id="contact_store">
    
    6
    +    <columns>
    
    7
    +      <!-- column-name Avatar -->
    
    8
    +      <column type="CairoGObjectSurface"/>
    
    9
    +      <!-- column-name Text -->
    
    10
    +      <column type="gchararray"/>
    
    11
    +      <!-- column-name Event -->
    
    12
    +      <column type="gboolean"/>
    
    13
    +      <!-- column-name IsContact -->
    
    14
    +      <column type="gboolean"/>
    
    15
    +      <!-- column-name GroupOrNickname -->
    
    16
    +      <column type="gchararray"/>
    
    17
    +    </columns>
    
    18
    +  </object>
    
    19
    +  <object class="GtkTreeView" id="roster_treeview">
    
    20
    +    <property name="visible">True</property>
    
    21
    +    <property name="can-focus">True</property>
    
    22
    +    <property name="model">contact_store</property>
    
    23
    +    <property name="headers-visible">False</property>
    
    24
    +    <property name="expander-column">expander</property>
    
    25
    +    <property name="search-column">1</property>
    
    26
    +    <signal name="button-press-event" handler="_on_roster_button_press_event" swapped="no"/>
    
    27
    +    <signal name="focus-out-event" handler="_on_focus_out" swapped="no"/>
    
    28
    +    <signal name="row-activated" handler="_on_roster_row_activated" swapped="no"/>
    
    29
    +    <child internal-child="selection">
    
    30
    +      <object class="GtkTreeSelection"/>
    
    31
    +    </child>
    
    32
    +    <child>
    
    33
    +      <object class="GtkTreeViewColumn" id="contact_column">
    
    34
    +        <property name="sizing">fixed</property>
    
    35
    +        <property name="fixed-width">210</property>
    
    36
    +        <property name="title">avatar</property>
    
    37
    +        <property name="expand">True</property>
    
    38
    +        <child>
    
    39
    +          <object class="GtkCellRendererPixbuf" id="avatar_renderer">
    
    40
    +            <property name="width">40</property>
    
    41
    +            <property name="ypad">3</property>
    
    42
    +            <property name="xalign">0</property>
    
    43
    +          </object>
    
    44
    +          <attributes>
    
    45
    +            <attribute name="visible">3</attribute>
    
    46
    +            <attribute name="surface">0</attribute>
    
    47
    +          </attributes>
    
    48
    +        </child>
    
    49
    +        <child>
    
    50
    +          <object class="GtkCellRendererText" id="text_renderer">
    
    51
    +            <property name="ellipsize">end</property>
    
    52
    +          </object>
    
    53
    +          <attributes>
    
    54
    +            <attribute name="markup">1</attribute>
    
    55
    +          </attributes>
    
    56
    +        </child>
    
    57
    +      </object>
    
    58
    +    </child>
    
    59
    +    <child>
    
    60
    +      <object class="GtkTreeViewColumn" id="event_column">
    
    61
    +        <property name="sizing">fixed</property>
    
    62
    +        <property name="fixed-width">35</property>
    
    63
    +        <property name="title">event</property>
    
    64
    +        <child>
    
    65
    +          <object class="GtkCellRendererPixbuf" id="icon">
    
    66
    +            <property name="xalign">0</property>
    
    67
    +            <property name="icon-name">gajim-event</property>
    
    68
    +          </object>
    
    69
    +          <attributes>
    
    70
    +            <attribute name="visible">2</attribute>
    
    71
    +          </attributes>
    
    72
    +        </child>
    
    73
    +      </object>
    
    74
    +    </child>
    
    75
    +    <child>
    
    76
    +      <object class="GtkTreeViewColumn" id="expander">
    
    77
    +        <property name="visible">False</property>
    
    78
    +        <property name="sizing">fixed</property>
    
    79
    +        <property name="title">expander</property>
    
    80
    +      </object>
    
    81
    +    </child>
    
    82
    +  </object>
    
    83
    +</interface>

  • gajim/gtk/account_page.py
    ... ... @@ -17,6 +17,7 @@
    17 17
     from gajim.common import app
    
    18 18
     from gajim.common.i18n import _
    
    19 19
     
    
    20
    +from .roster import Roster
    
    20 21
     from .status_selector import StatusSelector
    
    21 22
     from .util import open_window
    
    22 23
     
    
    ... ... @@ -46,7 +47,12 @@ def __init__(self, account):
    46 47
             box.add(settings_button)
    
    47 48
             box.add(self._status_selector)
    
    48 49
     
    
    49
    -        self.add(box)
    
    50
    +        self._roster = Roster(account)
    
    51
    +        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
    
    52
    +        hbox.add(self._roster)
    
    53
    +        hbox.add(box)
    
    54
    +
    
    55
    +        self.add(hbox)
    
    50 56
             self.show_all()
    
    51 57
             self.update()
    
    52 58
     
    
    ... ... @@ -59,3 +65,6 @@ def update(self):
    59 65
                 self._account, 'account_label')
    
    60 66
             self._account_label.set_text(account_label)
    
    61 67
             self._status_selector.update()
    
    68
    +
    
    69
    +    def process_event(self, event):
    
    70
    +        self._roster.process_event(event)

  • gajim/gtk/main.py
    ... ... @@ -54,20 +54,22 @@ def __init__(self):
    54 54
     
    
    55 55
             self.add(self._ui.main_grid)
    
    56 56
     
    
    57
    +        surface = load_icon('org.gajim.Gajim', self, 40)
    
    58
    +        self._ui.app_image.set_from_surface(surface)
    
    59
    +
    
    57 60
             self._chat_stack = ChatStack()
    
    61
    +        self._ui.right_grid.add(self._chat_stack)
    
    62
    +
    
    58 63
             self._chat_list_stack = ChatListStack(self, self._ui, self._chat_stack)
    
    59 64
             self._ui.chat_list_scrolled.add(self._chat_list_stack)
    
    60 65
     
    
    61
    -        self._account_side_bar = AccountSideBar()
    
    62 66
             self._workspace_side_bar = WorkspaceSideBar(self._chat_list_stack)
    
    63
    -
    
    64
    -        surface = load_icon('org.gajim.Gajim', self, 40)
    
    65
    -        self._ui.app_image.set_from_surface(surface)
    
    66 67
             self._ui.workspace_scrolled.add(self._workspace_side_bar)
    
    67 68
     
    
    69
    +        self._account_side_bar = AccountSideBar()
    
    68 70
             self._ui.account_box.add(self._account_side_bar)
    
    69 71
     
    
    70
    -        self._ui.right_grid.add(self._chat_stack)
    
    72
    +        self._account_pages = {}
    
    71 73
     
    
    72 74
             workspace_menu = Gio.Menu()
    
    73 75
             for action, label in WORKSPACE_MENU_DICT.items():
    
    ... ... @@ -419,6 +421,8 @@ def move_chat_to_workspace(self, new_workspace_id, account, jid):
    419 421
             current_chatlist.remove_chat(account, jid)
    
    420 422
             new_chatlist = self._chat_list_stack.get_chatlist(new_workspace_id)
    
    421 423
             new_chatlist.add_chat(account, jid, type_)
    
    424
    +        self._chat_list_stack.store_open_chats(current_chatlist.workspace_id)
    
    425
    +        self._chat_list_stack.store_open_chats(new_workspace_id)
    
    422 426
     
    
    423 427
         def _remove_chat(self, _action, param):
    
    424 428
             workspace_id, account, jid = param.unpack()
    
    ... ... @@ -445,8 +449,9 @@ def get_active_control(self):
    445 449
     
    
    446 450
         def _add_accounts(self):
    
    447 451
             for account in list(app.connections.keys()):
    
    448
    -            self._ui.main_stack.add_named(
    
    449
    -                AccountPage(account), account)
    
    452
    +            account_page = AccountPage(account)
    
    453
    +            self._account_pages[account] = account_page
    
    454
    +            self._ui.main_stack.add_named(account_page, account)
    
    450 455
     
    
    451 456
         def get_controls(self, account=None):
    
    452 457
             return self._chat_stack.get_controls(account)
    
    ... ... @@ -484,6 +489,9 @@ def _on_event(self, event):
    484 489
             if event.name == 'update-roster-avatar':
    
    485 490
                 return
    
    486 491
     
    
    492
    +        for page in self._account_pages.values():
    
    493
    +            page.process_event(event)
    
    494
    +
    
    487 495
             if not self.chat_exists(event.account, event.jid):
    
    488 496
                 if event.name == 'message-received':
    
    489 497
                     if event.properties.is_muc_pm:
    

  • gajim/gtk/roster.py
    1
    +import logging
    
    2
    +from typing import Optional
    
    3
    +from enum import IntEnum
    
    4
    +
    
    5
    +from gi.repository import GLib
    
    6
    +from gi.repository import Gtk
    
    7
    +from gi.repository import GObject
    
    8
    +
    
    9
    +from gajim.common import app
    
    10
    +from gajim.common.const import AvatarSize
    
    11
    +from gajim.common.i18n import _
    
    12
    +
    
    13
    +from .util import get_builder
    
    14
    +
    
    15
    +log = logging.getLogger('gajim.gui.roster')
    
    16
    +
    
    17
    +
    
    18
    +HANDLED_EVENTS = [
    
    19
    +    'presence-received',
    
    20
    +]
    
    21
    +
    
    22
    +
    
    23
    +class Column(IntEnum):
    
    24
    +    AVATAR = 0
    
    25
    +    TEXT = 1
    
    26
    +    EVENT = 2
    
    27
    +    IS_CONTACT = 3
    
    28
    +    NICK_OR_GROUP = 4
    
    29
    +
    
    30
    +
    
    31
    +class Roster(Gtk.ScrolledWindow):
    
    32
    +
    
    33
    +    __gsignals__ = {
    
    34
    +        'row-activated': (
    
    35
    +            GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
    
    36
    +            None,  # return value
    
    37
    +            (str, ))  # arguments
    
    38
    +    }
    
    39
    +
    
    40
    +    def __init__(self, account):
    
    41
    +        Gtk.ScrolledWindow.__init__(self)
    
    42
    +        self.set_size_request(300, -1)
    
    43
    +
    
    44
    +        self._account = account
    
    45
    +
    
    46
    +        self._handler_ids = {}
    
    47
    +
    
    48
    +        self._ui = get_builder('roster.ui')
    
    49
    +        self._ui.roster_treeview.set_model(None)
    
    50
    +        self.add(self._ui.roster_treeview)
    
    51
    +
    
    52
    +        # Holds the Gtk.TreeRowReference for each contact
    
    53
    +        self._contact_refs = {}
    
    54
    +        # Holds the Gtk.TreeRowReference for each group
    
    55
    +        self._group_refs = {}
    
    56
    +
    
    57
    +        self._store = self._ui.contact_store
    
    58
    +
    
    59
    +        self._roster = self._ui.roster_treeview
    
    60
    +
    
    61
    +        self.connect('destroy', self._on_destroy)
    
    62
    +        self._ui.connect_signals(self)
    
    63
    +
    
    64
    +        self._initial_draw()
    
    65
    +
    
    66
    +    def _on_roster_row_activated(self, _treeview, path, _column):
    
    67
    +        iter_ = self._store.get_iter(path)
    
    68
    +        if self._store.iter_parent(iter_) is None:
    
    69
    +            # This is a group row
    
    70
    +            return
    
    71
    +
    
    72
    +        nick = self._store[iter_][Column.NICK_OR_GROUP]
    
    73
    +        self.emit('row-activated', nick)
    
    74
    +
    
    75
    +    def _on_roster_button_press_event(self, treeview, event):
    
    76
    +        if event.button not in (2, 3):
    
    77
    +            return
    
    78
    +
    
    79
    +        pos = treeview.get_path_at_pos(int(event.x), int(event.y))
    
    80
    +        if pos is None:
    
    81
    +            return
    
    82
    +
    
    83
    +        path, _, _, _ = pos
    
    84
    +        iter_ = self._store.get_iter(path)
    
    85
    +        if self._store.iter_parent(iter_) is None:
    
    86
    +            # Group row
    
    87
    +            return
    
    88
    +
    
    89
    +        nick = self._store[iter_][Column.NICK_OR_GROUP]
    
    90
    +
    
    91
    +        if event.button == 3:  # right click
    
    92
    +            self._show_contact_menu(nick)
    
    93
    +
    
    94
    +        if event.button == 2:  # middle click
    
    95
    +            self.emit('row-activated', nick)
    
    96
    +
    
    97
    +    @staticmethod
    
    98
    +    def _on_focus_out(treeview, _param):
    
    99
    +        treeview.get_selection().unselect_all()
    
    100
    +
    
    101
    +    def _show_contact_menu(self, nick):
    
    102
    +        pass
    
    103
    +
    
    104
    +    def _on_destroy(self, _roster):
    
    105
    +        for id_ in list(self._handler_ids.keys()):
    
    106
    +            if self._handler_ids[id_].handler_is_connected(id_):
    
    107
    +                self._handler_ids[id_].disconnect(id_)
    
    108
    +            del self._handler_ids[id_]
    
    109
    +
    
    110
    +        self._contact_refs = {}
    
    111
    +        self._group_refs = {}
    
    112
    +        self._roster.set_model(None)
    
    113
    +        self._roster = None
    
    114
    +        self._store.clear()
    
    115
    +        self._store = None
    
    116
    +        # self._tooltip.destroy()
    
    117
    +        # self._tooltip = None
    
    118
    +
    
    119
    +    def set_model(self):
    
    120
    +        self._roster.set_model(self._store)
    
    121
    +
    
    122
    +    def _initial_draw(self):
    
    123
    +        client = app.get_client(self._account)
    
    124
    +        for contact in client.get_module('Roster').iter_contacts():
    
    125
    +            contact.connect('presence-update', self._on_presence_update)
    
    126
    +            contact.connect('avatar-update', self._on_avatar_update)
    
    127
    +            self._add_contact(contact)
    
    128
    +
    
    129
    +        self.set_model()
    
    130
    +        self._roster.expand_all()
    
    131
    +
    
    132
    +    def _on_presence_update(self, contact, _signal_name):
    
    133
    +        self.draw_avatar(contact)
    
    134
    +
    
    135
    +    def _on_avatar_update(self, contact, _signal_name):
    
    136
    +        self.draw_avatar(contact)
    
    137
    +
    
    138
    +    def _add_contact(self, contact):
    
    139
    +        main_group = _('Contacts')
    
    140
    +
    
    141
    +        if contact.groups:
    
    142
    +            for group_name in contact.groups:
    
    143
    +                group_iter = self._get_group_iter(group_name)
    
    144
    +                if not group_iter:
    
    145
    +                    group_iter = self._store.append(
    
    146
    +                        None, (None, group_name, None, False, group_name))
    
    147
    +                group_path = self._store.get_path(group_iter)
    
    148
    +                group_ref = Gtk.TreeRowReference(self._store, group_path)
    
    149
    +                self._group_refs[group_name] = group_ref
    
    150
    +        else:
    
    151
    +            group_iter = self._get_group_iter(main_group)
    
    152
    +            if not group_iter:
    
    153
    +                group_iter = self._store.append(
    
    154
    +                    None, (None, main_group, None, False, main_group))
    
    155
    +            group_path = self._store.get_path(group_iter)
    
    156
    +            group_ref = Gtk.TreeRowReference(self._store, group_path)
    
    157
    +            self._group_refs[main_group] = group_ref
    
    158
    +
    
    159
    +        # Avatar
    
    160
    +        surface = contact.get_avatar(AvatarSize.CHAT, self.get_scale_factor())
    
    161
    +
    
    162
    +        iter_ = self._store.append(
    
    163
    +            group_iter, (surface, contact.name, None, True, contact.name))
    
    164
    +        self._contact_refs[contact.jid] = Gtk.TreeRowReference(
    
    165
    +            self._store, self._store.get_path(iter_))
    
    166
    +
    
    167
    +        self.draw_groups()
    
    168
    +        self.draw_contact(contact.jid)
    
    169
    +
    
    170
    +        if (group_path is not None and
    
    171
    +                self._roster.get_model() is not None):
    
    172
    +            self._roster.expand_row(group_path, False)
    
    173
    +
    
    174
    +    def draw_groups(self):
    
    175
    +        for group in self._group_refs:
    
    176
    +            self.draw_group(group)
    
    177
    +
    
    178
    +    def draw_group(self, group):
    
    179
    +        group_iter = self._get_group_iter(group)
    
    180
    +        if not group_iter:
    
    181
    +            return
    
    182
    +
    
    183
    +        self._store[group_iter][Column.TEXT] = group
    
    184
    +
    
    185
    +    def draw_contacts(self):
    
    186
    +        for jid in self._contact_refs:
    
    187
    +            self.draw_contact(jid)
    
    188
    +
    
    189
    +    def draw_contact(self, jid):
    
    190
    +        iter_ = self._get_contact_iter(jid)
    
    191
    +        if not iter_:
    
    192
    +            return
    
    193
    +
    
    194
    +        client = app.get_client(self._account)
    
    195
    +        contact = client.get_module('Contacts').get_contact(jid)
    
    196
    +
    
    197
    +        self.draw_avatar(contact)
    
    198
    +        self._store[iter_][Column.TEXT] = GLib.markup_escape_text(contact.name)
    
    199
    +
    
    200
    +    def draw_avatar(self, contact):
    
    201
    +        iter_ = self._get_contact_iter(contact.jid)
    
    202
    +        if iter_ is None:
    
    203
    +            return
    
    204
    +
    
    205
    +        surface = contact.get_avatar(
    
    206
    +            AvatarSize.ROSTER, self.get_scale_factor())
    
    207
    +        self._store[iter_][Column.AVATAR] = surface
    
    208
    +
    
    209
    +    def _get_group_iter(self, group_name: str) -> Optional[Gtk.TreeIter]:
    
    210
    +        try:
    
    211
    +            ref = self._group_refs[group_name]
    
    212
    +        except KeyError:
    
    213
    +            return None
    
    214
    +
    
    215
    +        path = ref.get_path()
    
    216
    +        if path is None:
    
    217
    +            return None
    
    218
    +        return self._store.get_iter(path)
    
    219
    +
    
    220
    +    def _get_contact_iter(self, jid: str) -> Optional[Gtk.TreeIter]:
    
    221
    +        try:
    
    222
    +            ref = self._contact_refs[jid]
    
    223
    +        except KeyError:
    
    224
    +            return None
    
    225
    +
    
    226
    +        path = ref.get_path()
    
    227
    +        if path is None:
    
    228
    +            return None
    
    229
    +
    
    230
    +        return self._store.get_iter(path)
    
    231
    +
    
    232
    +    def clear(self):
    
    233
    +        self._contact_refs = {}
    
    234
    +        self._group_refs = {}
    
    235
    +        self._store.clear()
    
    236
    +
    
    237
    +    def _on_presence_received(self, event):
    
    238
    +        pass
    
    239
    +
    
    240
    +    def process_event(self, event):
    
    241
    +        if event.name not in HANDLED_EVENTS:
    
    242
    +            return
    
    243
    +
    
    244
    +        if event.name == 'presence-received':
    
    245
    +            self._on_presence_received(event)
    
    246
    +        else:
    
    247
    +            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):
    809 809
                 action, label = item
    
    810 810
                 action = f'win.{action}'
    
    811 811
                 menuitem = Gio.MenuItem.new(label, action)
    
    812
    -            variant_list = GLib.Variant('as', [workspace_id, account, jid])
    
    812
    +            variant_list = GLib.Variant(
    
    813
    +                'as', [workspace_id, account, str(jid)])
    
    813 814
                 menuitem.set_action_and_target_value(action, variant_list)
    
    814 815
                 menu.append_item(menuitem)
    
    815 816
             else:
    
    816 817
                 # This is a submenu
    
    817
    -            submenu = build_workspaces_submenu(workspace_id, account, jid)
    
    818
    +            submenu = build_workspaces_submenu(workspace_id, account, str(jid))
    
    818 819
                 menu.append_submenu(item[0], submenu)
    
    819 820
     
    
    820 821
         return menu