clients_icons.py 19.5 KB
Newer Older
1
import logging
2
from pathlib import Path
Dicson's avatar
Dicson committed
3

Dicson's avatar
Dicson committed
4
from gi.repository import Gtk
5
from nbxmpp import JID
Dicson's avatar
Dicson committed
6

7 8 9
from clients_icons import clients

from gajim.roster_window import Column
10

11
from gajim.plugins import GajimPlugin
12 13 14
from gajim.plugins.gui import GajimPluginConfigDialog
from gajim.plugins.plugins_i18n import _

15 16
from gajim.common import ged
from gajim.common import app
17
from gajim.common import caps_cache
Dicson's avatar
Dicson committed
18

19

20
from gajim.gtk.util import load_icon
21

Dicson's avatar
Dicson committed
22

Philipp Hörist's avatar
Philipp Hörist committed
23
log = logging.getLogger('gajim.p.client_icons')
Dicson's avatar
Dicson committed
24

25 26

class ClientsIconsPlugin(GajimPlugin):
Dicson's avatar
Dicson committed
27
    def init(self):
28
        self.description = _('Shows client icons in roster'
29
                             ' and in groupchats.')
30
        self.pos_list = [_('after statusicon'), _('before avatar')]
31
        self.events_handlers = {
32 33
            'caps-update': (ged.POSTGUI, self._on_caps_update),
        }
Dicson's avatar
Dicson committed
34
        self.gui_extension_points = {
Dicson's avatar
Dicson committed
35
            'groupchat_control': (self.connect_with_groupchat_control,
36
                                  self.disconnect_from_groupchat_control),
37 38 39
            'roster_draw_contact': (self.connect_with_roster_draw_contact, None),
            'roster_tooltip_populate': (self.connect_with_roster_tooltip_populate, None),
            'gc_tooltip_populate': (self.connect_with_gc_tooltip_populate, None),
40
            }
Dicson's avatar
Dicson committed
41
        self.config_default_values = {
42 43 44 45 46 47 48 49 50 51 52
            'show_in_roster': (True, ''),
            'show_in_groupchats': (True, ''),
            'show_in_tooltip': (True, ''),
            'show_unknown_icon': (True, ''),
            'pos_in_list': (0, ''),
            'show_facebook': (True, ''),
        }

        _icon_theme = Gtk.IconTheme.get_default()
        if _icon_theme is not None:
            _icon_theme.append_search_path(str(Path(__file__).parent))
Dicson's avatar
Dicson committed
53

54
        self.config_dialog = ClientsIconsPluginConfigDialog(self)
Dicson's avatar
Dicson committed
55

56 57
    @staticmethod
    def get_client_identity_name(contact):
58 59 60 61 62
        identities = contact.client_caps.get_cache_lookup_strategy()(
            caps_cache.capscache).identities
        if identities:
            for entry in identities:
                if entry['category'] == 'client':
63
                    return entry.get('name')
64 65 66 67 68 69 70

    @staticmethod
    def is_groupchat(contact):
        if hasattr(contact, 'is_groupchat'):
            return contact.is_groupchat()
        return False

71 72
    def add_tooltip_row(self, tooltip, contact, tooltip_grid):
        caps = contact.client_caps._node
73
        caps_image, client_name = self.get_icon(caps, contact, tooltip_grid)
74
        caps_image.set_halign(Gtk.PositionType.RIGHT)
75

76
        # fill clients table
Dicson's avatar
Dicson committed
77
        self.table = Gtk.Grid()
78 79
        self.table.set_name('client_icons_grid')
        self.table.set_property('column-spacing', 5)
Dicson's avatar
Dicson committed
80
        self.table.attach(caps_image, 1, 1, 1, 1)
81 82 83 84 85 86
        label_name = Gtk.Label()
        label_name.set_halign(Gtk.PositionType.RIGHT)
        label_name.set_markup(client_name)
        self.table.attach(label_name, 2, 1, 1, 1)
        self.table.show_all()

87
        # set label
Dicson's avatar
Dicson committed
88
        label = Gtk.Label()
89 90
        label.set_name('client_icons_label')
        label.set_halign(Gtk.PositionType.RIGHT)
91
        label.set_markup(_('Client:'))
92 93
        label.show()

94
        # set client table to tooltip
95 96
        tooltip_grid.insert_next_to(tooltip.resource_label,
                                    Gtk.PositionType.BOTTOM)
97 98 99 100 101 102 103 104 105
        tooltip_grid.attach_next_to(label, tooltip.resource_label,
                                    Gtk.PositionType.BOTTOM, 1, 1)
        tooltip_grid.attach_next_to(self.table, label,
                                    Gtk.PositionType.RIGHT, 1, 1)

    def connect_with_gc_tooltip_populate(self, tooltip, contact, tooltip_grid):
        if not self.config['show_in_tooltip']:
            return
        # Check if clients info already attached to tooltip
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120

        node = contact.client_caps._node
        image, client_name = self.get_icon(node, contact, tooltip_grid)
        label = Gtk.Label(label=client_name)

        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        box.add(image)
        box.add(label)
        box.show_all()

        tooltip_grid.insert_next_to(tooltip._ui.affiliation,
                                    Gtk.PositionType.BOTTOM)
        tooltip_grid.attach_next_to(box, tooltip._ui.affiliation,
                                    Gtk.PositionType.BOTTOM, 1, 1)

121
    def connect_with_roster_tooltip_populate(self, tooltip, contacts, tooltip_grid):
122 123
        if not self.config['show_in_tooltip']:
            return
124
        if len(contacts) == 1 and contacts[0].jid in app.get_our_jids():
125
            return
126
        if self.is_groupchat(contacts[0]):
127
            return
Dicson's avatar
Dicson committed
128

129 130 131 132
        # put contacts in dict, where key is priority
        num_resources = 0
        contacts_dict = {}
        for contact in contacts:
133 134
            if contact.show == 'offline':
                return
135 136
            if contact.resource:
                num_resources += 1
137 138
                if int(contact.priority) in contacts_dict:
                    contacts_dict[int(contact.priority)].append(contact)
139
                else:
140
                    contacts_dict[int(contact.priority)] = [contact]
141 142 143
        contact_keys = sorted(contacts_dict.keys())
        contact_keys.reverse()

144
        # fill clients table
145 146 147 148 149
        table = Gtk.Grid()
        table.insert_row(0)
        table.insert_row(0)
        table.insert_column(0)
        table.set_property('column-spacing', 2)
150

Dicson's avatar
Dicson committed
151 152 153 154
        vcard_current_row = 0
        for priority in contact_keys:
            for acontact in contacts_dict[priority]:
                caps = acontact.client_caps._node
155 156 157 158 159 160 161
                caps_image, client_name = self.get_icon(caps, acontact, tooltip_grid)
                caps_image.set_valign(Gtk.Align.START)
                table.attach(caps_image, 1, vcard_current_row, 1, 1)
                label = Gtk.Label(label=client_name)
                label.set_valign(Gtk.Align.START)
                label.set_xalign(0)
                table.attach(label, 2, vcard_current_row, 1, 1)
Dicson's avatar
Dicson committed
162
                vcard_current_row += 1
163 164
        table.show_all()
        table.set_valign(Gtk.Align.START)
165

166
        # set label
Dicson's avatar
Dicson committed
167
        label = Gtk.Label()
168 169
        label.set_halign(Gtk.Align.END)
        label.set_valign(Gtk.Align.START)
170
        if num_resources > 1:
171
            label.set_text(_('Clients:'))
172
        else:
173
            label.set_text(_('Client:'))
174
        label.show()
175
        # set clients table to tooltip
176
        tooltip_grid.insert_next_to(tooltip._ui.resource_label,
177
                                    Gtk.PositionType.BOTTOM)
178
        tooltip_grid.attach_next_to(label, tooltip._ui.resource_label,
179
                                    Gtk.PositionType.BOTTOM, 1, 1)
180
        tooltip_grid.attach_next_to(table, label,
181
                                    Gtk.PositionType.RIGHT, 1, 1)
182

183 184 185 186 187 188
    def get_icon(self, node, contact, widget):
        identity_name = self.get_client_identity_name(contact)
        client_name, icon_name = clients.get_data(identity_name, node)
        surface = load_icon(icon_name, widget=widget)
        return Gtk.Image.new_from_surface(surface), client_name

189 190 191 192 193
    def connect_with_roster_draw_contact(self, roster, jid, account, contact):
        if not self.active:
            return
        if not self.config['show_in_roster']:
            return
194
        if self.is_groupchat(contact):
195
            return
196
        child_iters = roster._get_contact_iter(jid, account, contact, roster.model)
197 198
        if not child_iters:
            return
199 200
        for iter_ in child_iters:
            if roster.model[iter_][self.renderer_num] is None:
201 202
                node = contact.client_caps._node
                self.set_icon(roster.model, iter_, self.renderer_num, node, contact)
203

Dicson's avatar
Dicson committed
204 205
    def connect_with_groupchat_control(self, chat_control):
        chat_control.nb_ext_renderers += 1
206
        chat_control.columns += [str]
Dicson's avatar
Dicson committed
207 208
        self.groupchats_tree_is_transformed = True
        self.chat_control = chat_control
Dicson's avatar
Dicson committed
209
        col = Gtk.TreeViewColumn()
Dicson's avatar
Dicson committed
210
        self.muc_renderer_num = 4 + chat_control.nb_ext_renderers
211 212
        client_icon_rend = (
            'client_icon', Gtk.CellRendererPixbuf(), False,
213
            'icon_name', self.muc_renderer_num,
214
            self.tree_cell_data_func, chat_control)
Dicson's avatar
Dicson committed
215 216 217
        # remove old column
        chat_control.list_treeview.remove_column(
            chat_control.list_treeview.get_column(0))
218 219 220
        # add new renderer in renderers list
        position_list = ['name', 'avatar']
        position = position_list[self.config['pos_in_list']]
Dicson's avatar
Dicson committed
221
        for renderer in chat_control.renderers_list:
222
            if renderer[0] == position:
Dicson's avatar
Dicson committed
223 224 225 226 227 228
                break
        num = chat_control.renderers_list.index(renderer)
        chat_control.renderers_list.insert(num, client_icon_rend)
        # fill and append column
        chat_control.fill_column(col)
        chat_control.list_treeview.insert_column(col, 0)
229

Dicson's avatar
Dicson committed
230
        chat_control.model = Gtk.TreeStore(*chat_control.columns)
231
        chat_control.model.set_sort_func(1, chat_control.tree_compare_iters)
Dicson's avatar
Dicson committed
232
        chat_control.model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
233
        chat_control.list_treeview.set_model(chat_control.model)
234

235
        # draw roster
236 237 238 239
        for nick in app.contacts.get_nick_list(
                chat_control.account, chat_control.room_jid):
            gc_contact = app.contacts.get_gc_contact(
                chat_control.account, chat_control.room_jid, nick)
240
            iter_ = chat_control.add_contact_to_roster(nick)
241 242 243
            if not self.config['show_in_groupchats']:
                continue
            caps = gc_contact.client_caps._node
244 245 246
            self.set_icon(
                chat_control.model, iter_,
                self.muc_renderer_num, caps, gc_contact)
247 248 249
        chat_control.draw_all_roles()
        # Recalculate column width for ellipsizin
        chat_control.list_treeview.columns_autosize()
Dicson's avatar
Dicson committed
250

251 252 253 254
    def disconnect_from_groupchat_control(self, gc_control):
        gc_control.nb_ext_renderers -= 1
        col = gc_control.list_treeview.get_column(0)
        gc_control.list_treeview.remove_column(col)
Dicson's avatar
Dicson committed
255
        col = Gtk.TreeViewColumn()
256 257 258 259 260 261 262
        for renderer in gc_control.renderers_list:
            if renderer[0] == 'client_icon':
                gc_control.renderers_list.remove(renderer)
                break
        gc_control.fill_column(col)
        gc_control.list_treeview.insert_column(col, 0)
        gc_control.columns = gc_control.columns[:self.muc_renderer_num] + \
Dicson's avatar
Dicson committed
263
            gc_control.columns[self.muc_renderer_num + 1:]
Dicson's avatar
Dicson committed
264
        gc_control.model = Gtk.TreeStore(*gc_control.columns)
Dicson's avatar
Dicson committed
265
        gc_control.model.set_sort_func(1, gc_control.tree_compare_iters)
Dicson's avatar
Dicson committed
266
        gc_control.model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
Dicson's avatar
Dicson committed
267
        gc_control.list_treeview.set_model(gc_control.model)
268
        gc_control.draw_roster()
Dicson's avatar
Dicson committed
269 270

    def activate(self):
Dicson's avatar
Dicson committed
271
        self.active = None
272
        roster = app.interface.roster
Dicson's avatar
Dicson committed
273
        col = Gtk.TreeViewColumn()
Dicson's avatar
Dicson committed
274
        roster.nb_ext_renderers += 1
275
        self.renderer_num = 11 + roster.nb_ext_renderers
Dicson's avatar
Dicson committed
276
        self.renderer = Gtk.CellRendererPixbuf()
277 278
        client_icon_rend = (
            'client_icon', self.renderer, False,
279 280
            'icon_name', self.renderer_num,
            self._roster_icon_renderer, self.renderer_num)
Dicson's avatar
Dicson committed
281 282
        # remove old column
        roster.tree.remove_column(roster.tree.get_column(0))
283 284 285
        # add new renderer in renderers list
        position_list = ['name', 'avatar']
        position = position_list[self.config['pos_in_list']]
Dicson's avatar
Dicson committed
286
        for renderer in roster.renderers_list:
287
            if renderer[0] == position:
Dicson's avatar
Dicson committed
288 289
                break
        num = roster.renderers_list.index(renderer)
Dicson's avatar
Dicson committed
290
        roster.renderers_list.insert(num, client_icon_rend)
Dicson's avatar
Dicson committed
291 292 293 294
        # fill and append column
        roster.fill_column(col)
        roster.tree.insert_column(col, 0)
        # redraw roster
295
        roster.columns += [str]
Dicson's avatar
Dicson committed
296
        self.active = True
Dicson's avatar
Dicson committed
297 298
        roster.setup_and_draw_roster()

299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    def _roster_icon_renderer(self, column, renderer, model, titer, data=None):
        try:
            type_ = model[titer][Column.TYPE]
        except TypeError:
            return

        # allocate space for the icon only if needed
        if model[titer][data] is None:
            renderer.set_property('visible', False)
        else:
            renderer.set_property('visible', True)

            if type_ == 'account':
                app.interface.roster._set_account_row_background_color(renderer)
                renderer.set_property('xalign', 1)
            elif type_:
                if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
                    # This can append at the moment we add the row
                    return
                jid = model[titer][Column.JID]
                account = model[titer][Column.ACCOUNT]
                app.interface.roster._set_contact_row_background_color(renderer, jid, account)

Dicson's avatar
Dicson committed
322
    def deactivate(self):
Dicson's avatar
Dicson committed
323
        self.active = None
324
        roster = app.interface.roster
Dicson's avatar
Dicson committed
325 326 327
        roster.nb_ext_renderers -= 1
        col = roster.tree.get_column(0)
        roster.tree.remove_column(col)
Dicson's avatar
Dicson committed
328
        col = Gtk.TreeViewColumn()
Dicson's avatar
Dicson committed
329 330 331 332 333 334
        for renderer in roster.renderers_list:
            if renderer[0] == 'client_icon':
                roster.renderers_list.remove(renderer)
                break
        roster.fill_column(col)
        roster.tree.insert_column(col, 0)
335
        roster.columns = roster.columns[:self.renderer_num] + roster.columns[
Dicson's avatar
Dicson committed
336
            self.renderer_num + 1:]
Dicson's avatar
Dicson committed
337 338
        roster.setup_and_draw_roster()

339 340 341
    def _on_caps_update(self, event):
        if event.conn.name == 'Local':
            # zeroconf
342
            return
343 344

        contact = self._get_contact_or_gc_contact_for_jid(event.conn.name, event.fjid)
345 346
        if contact is None:
            return
347

348 349 350 351 352 353
        if contact.is_gc_contact:
            self._draw_gc_contact(event, contact)
        else:
            self._draw_roster_contact(event, contact)

    def _draw_roster_contact(self, event, contact):
Dicson's avatar
Dicson committed
354 355
        if not self.config['show_in_roster']:
            return
356

357
        if contact.is_groupchat():
Dicson's avatar
Dicson committed
358
            return
359 360
        roster = app.interface.roster
        iters = roster._get_contact_iter(event.jid, event.conn.name, contact,
361
                                         roster.model)
362
        iter_ = iters[0]
363

364 365 366
        # highest contact changed
        caps = contact.client_caps._node
        if not caps:
367 368
            return

369
        if roster.model[iter_][self.renderer_num] is not None:
370 371
            self.set_icon(roster.model, iter_, self.renderer_num, caps, contact)
            return
Dicson's avatar
Dicson committed
372

373
        for iter_ in iters:
374
            self.set_icon(roster.model, iter_, self.renderer_num, caps, contact)
Dicson's avatar
Dicson committed
375

376
    def _draw_gc_contact(self, event, contact):
Dicson's avatar
Dicson committed
377 378
        if not self.config['show_in_groupchats']:
            return
379 380 381 382

        control = app.interface.msg_win_mgr.get_gc_control(contact.room_jid,
                                                           event.conn.name)
        if control is None:
Dicson's avatar
Dicson committed
383
            return
384 385 386 387 388
        iter_ = control.get_contact_iter(contact.name)
        if control.model[iter_][self.muc_renderer_num] is not None:
            return
        caps = contact.client_caps._node
        if not caps:
Dicson's avatar
Dicson committed
389
            return
390
        self.set_icon(control.model, iter_, self.muc_renderer_num, caps, contact)
Dicson's avatar
Dicson committed
391

392 393
    def _get_contact_or_gc_contact_for_jid(self, account, fjid):
        contact = app.contacts.get_contact_from_full_jid(account, fjid)
394

395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
        if contact is None:
            fjid = JID(fjid)
            room_jid, resource = fjid.getStripped(), fjid.getResource()
            contact = app.contacts.get_gc_contact(account, room_jid, resource)
        return contact

    def set_icon(self, model, iter_, pos, node, contact):
        identity_name = self.get_client_identity_name(contact)
        _client_name, icon_name = clients.get_data(identity_name, node)
        if 'unknown' in icon_name:
            if node is not None:
                log.warning('Unknown client: %s %s', identity_name, node)
            if not self.config['show_unknown_icon']:
                model[iter_][pos] = None
                return
410

411
        model[iter_][pos] = icon_name
Dicson's avatar
Dicson committed
412 413 414 415 416

    def tree_cell_data_func(self, column, renderer, model, iter_, control):
        if not model.iter_parent(iter_):
            renderer.set_property('visible', False)
            return
417 418

        if model[iter_][self.muc_renderer_num]:
419
            renderer.set_property('visible', True)
Dicson's avatar
Dicson committed
420

421 422
        contact = app.contacts.get_gc_contact(
            control.account, control.room_jid, model[iter_][1])
Dicson's avatar
Dicson committed
423 424 425
        if not contact:
            return

426
        bgcolor = app.config.get_per('themes', app.config.get(
Dicson's avatar
Dicson committed
427 428 429 430 431 432
            'roster_theme'), 'contactbgcolor')
        if bgcolor:
            renderer.set_property('cell-background', bgcolor)
        else:
            renderer.set_property('cell-background', None)
        renderer.set_property('width', 16)
Dicson's avatar
Dicson committed
433

Dicson's avatar
Dicson committed
434

Dicson's avatar
Dicson committed
435 436
class ClientsIconsPluginConfigDialog(GajimPluginConfigDialog):
    def init(self):
Dicson's avatar
Dicson committed
437
        self.Gtk_BUILDER_FILE_PATH = self.plugin.local_file_path(
Dicson's avatar
Dicson committed
438
                'config_dialog.ui')
Dicson's avatar
Dicson committed
439
        self.xml = Gtk.Builder()
440
        self.xml.set_translation_domain('gajim_plugins')
441
        self.xml.add_objects_from_file(self.Gtk_BUILDER_FILE_PATH, ['vbox1'])
Dicson's avatar
Dicson committed
442
        vbox = self.xml.get_object('vbox1')
Dicson's avatar
Dicson committed
443
        self.get_child().pack_start(vbox, True, True, 0)
444
        self.combo = self.xml.get_object('combobox1')
Dicson's avatar
Dicson committed
445
        self.liststore = Gtk.ListStore(str)
446
        self.combo.set_model(self.liststore)
Dicson's avatar
Dicson committed
447
        cellrenderer = Gtk.CellRendererText()
448 449 450 451 452 453
        self.combo.pack_start(cellrenderer, True)
        self.combo.add_attribute(cellrenderer, 'text', 0)

        for item in self.plugin.pos_list:
            self.liststore.append((item,))
        self.combo.set_active(self.plugin.config['pos_in_list'])
Dicson's avatar
Dicson committed
454

455 456 457 458 459 460
        self.xml.get_object('show_in_roster').set_active(
            self.plugin.config['show_in_roster'])
        self.xml.get_object('show_in_groupchats').set_active(
            self.plugin.config['show_in_groupchats'])
        self.xml.get_object('show_unknown_icon').set_active(
            self.plugin.config['show_unknown_icon'])
461 462
        self.xml.get_object('show_facebook').set_active(
            self.plugin.config['show_facebook'])
463 464
        self.xml.get_object('show_in_tooltip').set_active(
            self.plugin.config['show_in_tooltip'])
465

466 467 468
        self.xml.connect_signals(self)

    def redraw_all(self):
469 470
        self.plugin.deactivate()
        self.plugin.activate()
471
        for gc_control in app.interface.msg_win_mgr.get_controls('gc'):
472
            self.plugin.disconnect_from_groupchat_control(gc_control)
473
        for gc_control in app.interface.msg_win_mgr.get_controls('gc'):
474
            self.plugin.connect_with_groupchat_control(gc_control)
Dicson's avatar
Dicson committed
475 476 477

    def on_show_in_roster_toggled(self, widget):
        self.plugin.config['show_in_roster'] = widget.get_active()
478 479
        self.plugin.deactivate()
        self.plugin.activate()
Dicson's avatar
Dicson committed
480

481 482 483
    def on_show_in_tooltip_toggled(self, widget):
        self.plugin.config['show_in_tooltip'] = widget.get_active()

Dicson's avatar
Dicson committed
484 485
    def on_show_in_groupchats_toggled(self, widget):
        self.plugin.config['show_in_groupchats'] = widget.get_active()
486
        for gc_control in app.interface.msg_win_mgr.get_controls('gc'):
487
            self.plugin.disconnect_from_groupchat_control(gc_control)
488
        for gc_control in app.interface.msg_win_mgr.get_controls('gc'):
489
            self.plugin.connect_with_groupchat_control(gc_control)
Dicson's avatar
Dicson committed
490 491 492

    def on_show_unknown_icon_toggled(self, widget):
        self.plugin.config['show_unknown_icon'] = widget.get_active()
493
        self.redraw_all()
494

495 496 497 498
    def on_show_facebook_toggled(self, widget):
        self.plugin.config['show_facebook'] = widget.get_active()
        self.redraw_all()

499 500
    def on_combobox1_changed(self, widget):
        self.plugin.config['pos_in_list'] = widget.get_active()
501
        self.redraw_all()