roster_window.py 232 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
#                    Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
#                         Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
#                    James Newton <redshodan AT gmail.com>
#                    Tomasz Melcer <liori AT exroot.org>
#                    Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#                    Jonathan Schleifer <js-gajim AT webkeks.org>
#
# 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/>.
30

31
32
33
34
35
36
37
import os
import sys
import time
import locale
import logging
from enum import IntEnum, unique

38
39
40
41
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from gi.repository import GObject
Yann Leboulanger's avatar
Yann Leboulanger committed
42
from gi.repository import GLib
43
from gi.repository import Gio
Philipp Hörist's avatar
Philipp Hörist committed
44
45
46
from nbxmpp.protocol import NS_ROSTERX
from nbxmpp.protocol import NS_CONFERENCE
from nbxmpp.protocol import NS_JINGLE_FILE_TRANSFER_5
47
from nbxmpp.structs import MoodData
Philipp Hörist's avatar
Philipp Hörist committed
48
from nbxmpp.structs import ActivityData
49

André's avatar
André committed
50
51
52
53
from gajim import dialogs
from gajim import vcard
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
Philipp Hörist's avatar
Philipp Hörist committed
54

55
from gajim.common import app
André's avatar
André committed
56
from gajim.common import helpers
Philipp Hörist's avatar
Philipp Hörist committed
57
from gajim.common import idle
André's avatar
André committed
58
59
from gajim.common.exceptions import GajimGeneralException
from gajim.common import i18n
60
from gajim.common.helpers import save_roster_position
61
from gajim.common.i18n import _
Philipp Hörist's avatar
Philipp Hörist committed
62
from gajim.common.const import PEPEventType, AvatarSize, StyleAttr
Philipp Hörist's avatar
Philipp Hörist committed
63
64
from gajim.common.dbus import location

André's avatar
André committed
65
66
from gajim.common import ged
from gajim.message_window import MessageWindowMgr
67

68
69
70
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.dialogs import NewConfirmationCheckDialog
71
72
73
74
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import InputDialog
from gajim.gtk.dialogs import WarningDialog
from gajim.gtk.dialogs import InformationDialog
75
from gajim.gtk.dialogs import InvitationReceivedDialog
76
77
78
79
from gajim.gtk.single_message import SingleMessageWindow
from gajim.gtk.add_contact import AddNewContactWindow
from gajim.gtk.account_wizard import AccountCreationWizard
from gajim.gtk.service_registration import ServiceRegistration
80
from gajim.gtk.discovery import ServiceDiscoveryWindow
81
from gajim.gtk.tooltips import RosterTooltip
82
from gajim.gtk.adhoc import AdHocCommand
83
from gajim.gtk.util import get_icon_name
84
from gajim.gtk.util import resize_window
85
from gajim.gtk.util import restore_roster_position
86
from gajim.gtk.util import get_metacontact_surface
87
from gajim.gtk.util import get_builder
88
from gajim.gtk.util import set_urgency_hint
Philipp Hörist's avatar
Philipp Hörist committed
89
from gajim.gtk.util import get_activity_icon_name
90
from gajim.gtk.util import open_window
91

92

93
94
log = logging.getLogger('gajim.roster')

95
@unique
96
97
class Column(IntEnum):
    IMG = 0  # image to show state (online, new message etc)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
98
    NAME = 1  # cellrenderer text that holds contact nickname
99
100
101
102
103
    TYPE = 2  # account, group or contact?
    JID = 3  # the jid of the row
    ACCOUNT = 4  # cellrenderer text that holds account name
    MOOD_PIXBUF = 5
    ACTIVITY_PIXBUF = 6
104
105
    TUNE_ICON = 7
    LOCATION_ICON = 8
106
    AVATAR_IMG = 9  # avatar_sha
107
    PADLOCK_PIXBUF = 10  # use for account row only
108
    VISIBLE = 11
109

Dicson's avatar
Dicson committed
110

111
class RosterWindow:
112
    """
Daniel Brötzmann's avatar
Daniel Brötzmann committed
113
    Class for main window of the GTK interface
114
115
116
117
    """

    def _get_account_iter(self, name, model=None):
        """
118
        Return the Gtk.TreeIter of the given account or None if not found
119
120
121
122
123
124

        Keyword arguments:
        name -- the account name
        model -- the data model (default TreeFilterModel)
        """
        if model is None:
125
126
127
128
            model = self.modelfilter
            if model is None:
                return

129
        if self.regroup:
130
            name = 'MERGED'
131
132
        if name not in self._iters:
            return None
133
        it = self._iters[name]['account']
134
135
136
137

        if model == self.model or it is None:
            return it
        try:
138
139
140
141
            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
            if ok:
                return it
            return None
142
143
        except RuntimeError:
            return None
144
145


146
    def _get_group_iter(self, name, account, model=None):
147
        """
148
        Return the Gtk.TreeIter of the given group or None if not found
149
150
151
152
153
154

        Keyword arguments:
        name -- the group name
        account -- the account name
        model -- the data model (default TreeFilterModel)
        """
155
        if model is None:
156
            model = self.modelfilter
157
158
159
            if model is None:
                return

160
161
162
        if self.regroup:
            account = 'MERGED'

163
164
        if account not in self._iters:
            return None
165
166
167
168
169
170
171
        if name not in self._iters[account]['groups']:
            return None

        it = self._iters[account]['groups'][name]
        if model == self.model or it is None:
            return it
        try:
172
173
174
175
            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
            if ok:
                return it
            return None
176
177
        except RuntimeError:
            return None
178
179
180
181


    def _get_self_contact_iter(self, account, model=None):
        """
182
        Return the Gtk.TreeIter of SelfContact or None if not found
183
184
185
186
187

        Keyword arguments:
        account -- the account of SelfContact
        model -- the data model (default TreeFilterModel)
        """
188
        jid = app.get_jid_from_account(account)
189
190
191
        its = self._get_contact_iter(jid, account, model=model)
        if its:
            return its[0]
192
193
194
195
196
        return None


    def _get_contact_iter(self, jid, account, contact=None, model=None):
        """
197
        Return a list of Gtk.TreeIter of the given contact
198
199
200
201
202
203
204

        Keyword arguments:
        jid -- the jid without resource
        account -- the account
        contact -- the contact (default None)
        model -- the data model (default TreeFilterModel)
        """
205
        if model is None:
206
207
208
209
210
211
            model = self.modelfilter
            # when closing Gajim model can be none (async pbs?)
            if model is None:
                return []

        if not contact:
212
            contact = app.contacts.get_first_contact_from_jid(account, jid)
213
214
215
216
            if not contact:
                # We don't know this contact
                return []

217
218
219
        if account not in self._iters:
            return []

220
221
222
223
224
225
226
227
228
229
230
231
232
233
        if jid not in self._iters[account]['contacts']:
            return []

        its = self._iters[account]['contacts'][jid]

        if not its:
            return []

        if model == self.model:
            return its

        its2 = []
        for it in its:
            try:
234
235
236
                (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
                if ok:
                    its2.append(it)
237
238
239
            except RuntimeError:
                pass
        return its2
240

Philipp Hörist's avatar
Philipp Hörist committed
241
242
    @staticmethod
    def _iter_is_separator(model, titer):
243
244
245
246
247
        """
        Return True if the given iter is a separator

        Keyword arguments:
        model -- the data model
248
        iter -- the Gtk.TreeIter to test
249
250
251
252
253
        """
        if model[titer][0] == 'SEPARATOR':
            return True
        return False

Philipp Hörist's avatar
Philipp Hörist committed
254
255
256
257
258
259
260
261
262
    @staticmethod
    def _status_cell_data_func(cell_layout, cell, tree_model, iter_):
        if isinstance(cell, Gtk.CellRendererPixbuf):
            icon_name = tree_model[iter_][1]
            if icon_name is None:
                return
            if tree_model[iter_][2] == 'status':
                cell.set_property('icon_name', icon_name)
            else:
263
                iconset_name = get_icon_name(icon_name)
Philipp Hörist's avatar
Philipp Hörist committed
264
265
266
267
268
269
270
271
272
                cell.set_property('icon_name', iconset_name)
        else:
            show = tree_model[iter_][0]
            id_ = tree_model[iter_][2]
            if id_ not in ('status', 'desync'):
                show = helpers.get_uf_show(show)
            cell.set_property('text', show)


273

Yann Leboulanger's avatar
Yann Leboulanger committed
274
#############################################################################
275
### Methods for adding and removing roster window items
steve-e's avatar
steve-e committed
276
#############################################################################
Yann Leboulanger's avatar
Yann Leboulanger committed
277

278
279
280
281
282
283
284
285
286
287
288
    def add_account(self, account):
        """
        Add account to roster and draw it. Do nothing if it is already in
        """
        if self._get_account_iter(account):
            # Will happen on reconnect or for merged accounts
            return

        if self.regroup:
            # Merged accounts view
            show = helpers.get_global_show()
289
            it = self.model.append(None, [get_icon_name(show),
290
                _('Merged accounts'), 'account', '', 'all', None, None, None,
291
                None, None, None, True] + [None] * self.nb_ext_renderers)
292
            self._iters['MERGED']['account'] = it
293
        else:
294
295
            show = app.SHOW_LIST[app.connections[account].connected]
            our_jid = app.get_jid_from_account(account)
296
297

            tls_pixbuf = None
298
            if app.account_is_securely_connected(account):
Philipp Hörist's avatar
Philipp Hörist committed
299
                tls_pixbuf = 'changes-prevent'
300

301
            it = self.model.append(None, [get_icon_name(show),
Yann Leboulanger's avatar
Yann Leboulanger committed
302
                GLib.markup_escape_text(account), 'account', our_jid,
303
                account, None, None, None, None, None, tls_pixbuf, True] +
304
                [None] * self.nb_ext_renderers)
305
            self._iters[account]['account'] = it
306
307
308
309

        self.draw_account(account)


310
311
    def add_account_contacts(self, account, improve_speed=True,
    draw_contacts=True):
312
        """
313
314
        Add all contacts and groups of the given account to roster, draw them
        and account
315
        """
316
317
        if improve_speed:
            self._before_fill()
318
        jids = app.contacts.get_jid_list(account)
319
320
321
322

        for jid in jids:
            self.add_contact(jid, account)

323
324
325
326
327
        if draw_contacts:
            # Do not freeze the GUI when drawing the contacts
            if jids:
                # Overhead is big, only invoke when needed
                self._idle_draw_jids_of_account(jids, account)
328

329
            # Draw all known groups
330
            for group in app.groups[account]:
331
332
                self.draw_group(group, account)
            self.draw_account(account)
333

334
335
        if improve_speed:
            self._after_fill()
336

337
338
339
340
341
342
343
344
    def _add_group_iter(self, account, group):
        """
        Add a group iter in roster and return the newly created iter
        """
        if self.regroup:
            account_group = 'MERGED'
        else:
            account_group = account
Philipp Hörist's avatar
Philipp Hörist committed
345
        delimiter = app.connections[account].get_module('Delimiter').delimiter
346
347
        group_splited = group.split(delimiter)
        parent_group = delimiter.join(group_splited[:-1])
348
        if len(group_splited) > 1 and parent_group in self._iters[account_group]['groups']:
349
350
351
            iter_parent = self._iters[account_group]['groups'][parent_group]
        elif parent_group:
            iter_parent = self._add_group_iter(account, parent_group)
352
            if parent_group not in app.groups[account]:
353
354
355
356
                if account + parent_group in self.collapsed_rows:
                    is_expanded = False
                else:
                    is_expanded = True
357
                app.groups[account][parent_group] = {'expand': is_expanded}
358
359
360
        else:
            iter_parent = self._get_account_iter(account, self.model)
        iter_group = self.model.append(iter_parent,
361
            [get_icon_name('closed'),
Yann Leboulanger's avatar
Yann Leboulanger committed
362
            GLib.markup_escape_text(group), 'group', group, account, None,
363
            None, None, None, None, None, False] + [None] * self.nb_ext_renderers)
364
365
366
        self.draw_group(group, account)
        self._iters[account_group]['groups'][group] = iter_group
        return iter_group
367
368

    def _add_entity(self, contact, account, groups=None,
369
    big_brother_contact=None, big_brother_account=None):
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
        """
        Add the given contact to roster data model

        Contact is added regardless if he is already in roster or not. Return
        list of newly added iters.

        Keyword arguments:
        contact -- the contact to add
        account -- the contacts account
        groups -- list of groups to add the contact to.
                  (default groups in contact.get_shown_groups()).
                Parameter ignored when big_brother_contact is specified.
        big_brother_contact -- if specified contact is added as child
                  big_brother_contact. (default None)
        """
        added_iters = []
386
        visible = self.contact_is_visible(contact, account)
387
388
389
390
391
392
393
394
        if big_brother_contact:
            # Add contact under big brother

            parent_iters = self._get_contact_iter(
                    big_brother_contact.jid, big_brother_account,
                    big_brother_contact, self.model)

            # Do not confuse get_contact_iter: Sync groups of family members
395
            contact.groups = big_brother_contact.groups[:]
396

397
398
            image = self._get_avatar_image(account, contact.jid)

399
            for child_iter in parent_iters:
400
                it = self.model.append(child_iter, [None,
401
                    contact.get_shown_name(), 'contact', contact.jid, account,
402
                    None, None, None, None, image, None, visible] + \
403
                    [None] * self.nb_ext_renderers)
404
                added_iters.append(it)
405
406
407
408
                if contact.jid in self._iters[account]['contacts']:
                    self._iters[account]['contacts'][contact.jid].append(it)
                else:
                    self._iters[account]['contacts'][contact.jid] = [it]
409
410
411
412
413
414
        else:
            # We are a normal contact. Add us to our groups.
            if not groups:
                groups = contact.get_shown_groups()
            for group in groups:
                child_iterG = self._get_group_iter(group, account,
415
                    model=self.model)
416
417
                if not child_iterG:
                    # Group is not yet in roster, add it!
418
                    child_iterG = self._add_group_iter(account, group)
419
420
421

                if contact.is_transport():
                    typestr = 'agent'
422
                elif contact.is_groupchat:
423
424
425
426
                    typestr = 'groupchat'
                else:
                    typestr = 'contact'

427
428
                image = self._get_avatar_image(account, contact.jid)

429
430
                # we add some values here. see draw_contact
                # for more
431
432
                i_ = self.model.append(child_iterG, [None,
                    contact.get_shown_name(), typestr, contact.jid, account,
433
                    None, None, None, None, image, None, visible] + \
434
                    [None] * self.nb_ext_renderers)
435
                added_iters.append(i_)
436
437
438
439
                if contact.jid in self._iters[account]['contacts']:
                    self._iters[account]['contacts'][contact.jid].append(i_)
                else:
                    self._iters[account]['contacts'][contact.jid] = [i_]
440
441
442
443
444
445

                # Restore the group expand state
                if account + group in self.collapsed_rows:
                    is_expanded = False
                else:
                    is_expanded = True
446
447
                if group not in app.groups[account]:
                    app.groups[account][group] = {'expand': is_expanded}
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464

        return added_iters

    def _remove_entity(self, contact, account, groups=None):
        """
        Remove the given contact from roster data model

        Empty groups after contact removal are removed too.
        Return False if contact still has children and deletion was
        not performed.
        Return True on success.

        Keyword arguments:
        contact -- the contact to add
        account -- the contacts account
        groups -- list of groups to remove the contact from.
        """
465
466
        iters = self._get_contact_iter(contact.jid, account, contact,
            self.model)
467
468

        parent_iter = self.model.iter_parent(iters[0])
469
        parent_type = self.model[parent_iter][Column.TYPE]
470
471
472
473
474
475
476
477
478
479
480
481
482
483

        if groups:
            # Only remove from specified groups
            all_iters = iters[:]
            group_iters = [self._get_group_iter(group, account)
                    for group in groups]
            iters = [titer for titer in all_iters
                    if self.model.iter_parent(titer) in group_iters]

        iter_children = self.model.iter_children(iters[0])

        if iter_children:
            # We have children. We cannot be removed!
            return False
Yann Leboulanger's avatar
Yann Leboulanger committed
484
485
486
        # Remove us and empty groups from the model
        for i in iters:
            parent_i = self.model.iter_parent(i)
487
            parent_type = self.model[parent_i][Column.TYPE]
Yann Leboulanger's avatar
Yann Leboulanger committed
488
489
490
491
492
493
494
495

            to_be_removed = i
            while parent_type == 'group' and \
            self.model.iter_n_children(parent_i) == 1:
                if self.regroup:
                    account_group = 'MERGED'
                else:
                    account_group = account
496
                group = self.model[parent_i][Column.JID]
497
498
                if group in app.groups[account]:
                    del app.groups[account][group]
Yann Leboulanger's avatar
Yann Leboulanger committed
499
500
501
                to_be_removed = parent_i
                del self._iters[account_group]['groups'][group]
                parent_i = self.model.iter_parent(parent_i)
502
                parent_type = self.model[parent_i][Column.TYPE]
Yann Leboulanger's avatar
Yann Leboulanger committed
503
            self.model.remove(to_be_removed)
504

Yann Leboulanger's avatar
Yann Leboulanger committed
505
506
        del self._iters[account]['contacts'][contact.jid]
        return True
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521

    def _add_metacontact_family(self, family, account):
        """
        Add the give Metacontact family to roster data model

        Add Big Brother to his groups and all others under him.
        Return list of all added (contact, account) tuples with
        Big Brother as first element.

        Keyword arguments:
        family -- the family, see Contacts.get_metacontacts_family()
        """

        nearby_family, big_brother_jid, big_brother_account = \
                self._get_nearby_family_and_big_brother(family, account)
522
523
        if not big_brother_jid:
            return []
524
        big_brother_contact = app.contacts.get_first_contact_from_jid(
525
526
527
528
529
530
531
532
533
                big_brother_account, big_brother_jid)

        self._add_entity(big_brother_contact, big_brother_account)

        brothers = []
        # Filter family members
        for data in nearby_family:
            _account = data['account']
            _jid = data['jid']
534
            _contact = app.contacts.get_first_contact_from_jid(
535
536
537
538
539
540
541
542
                    _account, _jid)

            if not _contact or _contact == big_brother_contact:
                # Corresponding account is not connected
                # or brother already added
                continue

            self._add_entity(_contact, _account,
543
544
                    big_brother_contact=big_brother_contact,
                    big_brother_account=big_brother_account)
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
            brothers.append((_contact, _account))

        brothers.insert(0, (big_brother_contact, big_brother_account))
        return brothers

    def _remove_metacontact_family(self, family, account):
        """
        Remove the given Metacontact family from roster data model

        See Contacts.get_metacontacts_family() and
        RosterWindow._remove_entity()
        """
        nearby_family = self._get_nearby_family_and_big_brother(
                family, account)[0]

        # Family might has changed (actual big brother not on top).
Alexander Krotov's avatar
Alexander Krotov committed
561
        # Remove children first then big brother
562
563
564
565
        family_in_roster = False
        for data in nearby_family:
            _account = data['account']
            _jid = data['jid']
566
            _contact = app.contacts.get_first_contact_from_jid(_account, _jid)
567
568
569
570
571
572
573
574
575
576

            iters = self._get_contact_iter(_jid, _account, _contact, self.model)
            if not iters or not _contact:
                # Family might not be up to date.
                # Only try to remove what is actually in the roster
                continue

            family_in_roster = True

            parent_iter = self.model.iter_parent(iters[0])
577
            parent_type = self.model[parent_iter][Column.TYPE]
578
579
580
581
582
583
584

            if parent_type != 'contact':
                # The contact on top
                old_big_account = _account
                old_big_contact = _contact
                continue

Philipp Hörist's avatar
Philipp Hörist committed
585
            self._remove_entity(_contact, _account)
586
587
588
589

        if not family_in_roster:
            return False

Philipp Hörist's avatar
Philipp Hörist committed
590
        self._remove_entity(old_big_contact, old_big_account)
591
592
593
594
595
596
597
598
599
600

        return True

    def _recalibrate_metacontact_family(self, family, account):
        """
        Regroup metacontact family if necessary
        """

        brothers = []
        nearby_family, big_brother_jid, big_brother_account = \
601
            self._get_nearby_family_and_big_brother(family, account)
602
        big_brother_contact = app.contacts.get_contact(big_brother_account,
603
604
605
            big_brother_jid)
        child_iters = self._get_contact_iter(big_brother_jid,
            big_brother_account, model=self.model)
606
607
        if child_iters:
            parent_iter = self.model.iter_parent(child_iters[0])
608
            parent_type = self.model[parent_iter][Column.TYPE]
609
610
611
612
613

            # Check if the current BigBrother has even been before.
            if parent_type == 'contact':
                for data in nearby_family:
                    # recalibrate after remove to keep highlight
614
                    if data['jid'] in app.to_be_removed[data['account']]:
615
616
617
618
619
620
621
622
623
624
625
626
627
628
                        return

                self._remove_metacontact_family(family, account)
                brothers = self._add_metacontact_family(family, account)

                for c, acc in brothers:
                    self.draw_completely(c.jid, acc)

        # Check is small brothers are under the big brother
        for child in nearby_family:
            _jid = child['jid']
            _account = child['account']
            if _account == big_brother_account and _jid == big_brother_jid:
                continue
629
630
            child_iters = self._get_contact_iter(_jid, _account,
                model=self.model)
631
632
633
            if not child_iters:
                continue
            parent_iter = self.model.iter_parent(child_iters[0])
634
            parent_type = self.model[parent_iter][Column.TYPE]
635
            if parent_type != 'contact':
636
                _contact = app.contacts.get_contact(_account, _jid)
637
638
639
640
641
642
                self._remove_entity(_contact, _account)
                self._add_entity(_contact, _account, groups=None,
                        big_brother_contact=big_brother_contact,
                        big_brother_account=big_brother_account)

    def _get_nearby_family_and_big_brother(self, family, account):
643
        return app.contacts.get_nearby_family_and_big_brother(family, account)
644
645
646
647
648
649
650

    def _add_self_contact(self, account):
        """
        Add account's SelfContact to roster and draw it and the account

        Return the SelfContact contact instance
        """
651
652
        jid = app.get_jid_from_account(account)
        contact = app.contacts.get_first_contact_from_jid(account, jid)
653
654

        child_iterA = self._get_account_iter(account, self.model)
655
        self._iters[account]['contacts'][jid] = [self.model.append(child_iterA,
656
            [None, app.nicks[account], 'self_contact', jid, account, None,
657
            None, None, None, None, None, True] + [None] * self.nb_ext_renderers)]
658
659
660
661
662
663
664

        self.draw_completely(jid, account)
        self.draw_account(account)

        return contact

    def redraw_metacontacts(self, account):
665
        for family in app.contacts.iter_metacontacts_families(account):
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
            self._recalibrate_metacontact_family(family, account)

    def add_contact(self, jid, account):
        """
        Add contact to roster and draw him

        Add contact to all its group and redraw the groups, the contact and the
        account. If it's a Metacontact, add and draw the whole family.
        Do nothing if the contact is already in roster.

        Return the added contact instance. If it is a Metacontact return
        Big Brother.

        Keyword arguments:
        jid -- the contact's jid or SelfJid to add SelfContact
        account -- the corresponding account.
        """
683
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
684
        if self._get_contact_iter(jid, account, contact, self.model):
685
686
687
            # If contact already in roster, do nothing
            return

688
        if jid == app.get_jid_from_account(account):
Philipp Hörist's avatar
Philipp Hörist committed
689
            return self._add_self_contact(account)
690
691
692
693

        is_observer = contact.is_observer()
        if is_observer:
            # if he has a tag, remove it
694
            app.contacts.remove_metacontact(account, jid)
695
696

        # Add contact to roster
697
        family = app.contacts.get_metacontacts_family(account, jid)
698
699
700
701
702
703
704
705
706
707
        contacts = []
        if family:
            # We have a family. So we are a metacontact.
            # Add all family members that we shall be grouped with
            if self.regroup:
                # remove existing family members to regroup them
                self._remove_metacontact_family(family, account)
            contacts = self._add_metacontact_family(family, account)
        else:
            # We are a normal contact
708
            contacts = [(contact, account), ]
709
710
711
712
713
714
715
716
717
718
719
720
721
            self._add_entity(contact, account)

        # Draw the contact and its groups contact
        if not self.starting:
            for c, acc in contacts:
                self.draw_completely(c.jid, acc)
            for group in contact.get_shown_groups():
                self.draw_group(group, account)
                self._adjust_group_expand_collapse_state(group, account)
            self.draw_account(account)

        return contacts[0][0] # it's contact/big brother with highest priority

Philipp Hörist's avatar
Philipp Hörist committed
722
    def remove_contact(self, jid, account, force=False, backend=False, maximize=False):
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
        """
        Remove contact from roster

        Remove contact from all its group. Remove empty groups or redraw
        otherwise.
        Draw the account.
        If it's a Metacontact, remove the whole family.
        Do nothing if the contact is not in roster.

        Keyword arguments:
        jid -- the contact's jid or SelfJid to remove SelfContact
        account -- the corresponding account.
        force -- remove contact even it has pending evens (Default False)
        backend -- also remove contact instance (Default False)
        """
738
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
739
740
741
        if not contact:
            return

742
743
744
        if not force and self.contact_has_pending_roster_events(contact,
        account):
            return False
745
746
747
748
749

        iters = self._get_contact_iter(jid, account, contact, self.model)
        if iters:
            # no more pending events
            # Remove contact from roster directly
750
            family = app.contacts.get_metacontacts_family(account, jid)
751
752
753
754
755
756
            if family:
                # We have a family. So we are a metacontact.
                self._remove_metacontact_family(family, account)
            else:
                self._remove_entity(contact, account)

757
758
        old_grps = []
        if backend:
759
            if not app.interface.msg_win_mgr.get_control(jid, account) or \
760
761
762
763
            force:
                # If a window is still opened: don't remove contact instance
                # Remove contact before redrawing, otherwise the old
                # numbers will still be show
Philipp Hörist's avatar
Philipp Hörist committed
764
                if not maximize:
Alexander Krotov's avatar
Alexander Krotov committed
765
                    # Don't remove contact when we maximize a room
Philipp Hörist's avatar
Philipp Hörist committed
766
                    app.contacts.remove_jid(account, jid, remove_meta=True)
767
768
769
770
771
772
773
774
775
776
                if iters:
                    rest_of_family = [data for data in family
                        if account != data['account'] or jid != data['jid']]
                    if rest_of_family:
                        # reshow the rest of the family
                        brothers = self._add_metacontact_family(rest_of_family,
                            account)
                        for c, acc in brothers:
                            self.draw_completely(c.jid, acc)
            else:
777
                for c in app.contacts.get_contacts(account, jid):
778
779
780
781
                    c.sub = 'none'
                    c.show = 'not in roster'
                    c.status = ''
                    old_grps = c.get_shown_groups()
782
                    c.groups = [_('Not in contact list')]
783
784
                    self._add_entity(c, account)
                    self.draw_contact(jid, account)
785
786
787

        if iters:
            # Draw all groups of the contact
788
            for group in contact.get_shown_groups() + old_grps:
789
790
791
792
793
794
795
796
797
798
799
800
801
802
                self.draw_group(group, account)
            self.draw_account(account)

        return True

    def rename_self_contact(self, old_jid, new_jid, account):
        """
        Rename the self_contact jid

        Keyword arguments:
        old_jid -- our old jid
        new_jid -- our new jid
        account -- the corresponding account.
        """
803
        app.contacts.change_contact_jid(old_jid, new_jid, account)
804
805
806
        self_iter = self._get_self_contact_iter(account, model=self.model)
        if not self_iter:
            return
807
        self.model[self_iter][Column.JID] = new_jid
808
809
        self.draw_contact(new_jid, account)

Philipp Hörist's avatar
Philipp Hörist committed
810
811
812
813
814
815
    def minimize_groupchat(self, account, jid, status=''):
        gc_control = app.interface.msg_win_mgr.get_gc_control(jid, account)
        app.interface.minimized_controls[account][jid] = gc_control
        self.add_groupchat(jid, account)

    def add_groupchat(self, jid, account):
816
817
818
        """
        Add groupchat to roster and draw it. Return the added contact instance
        """
Philipp Hörist's avatar
Philipp Hörist committed
819
820
        contact = app.contacts.get_groupchat_contact(account, jid)
        show = 'offline'
821
        if app.account_is_connected(account):
822
823
            show = 'online'

Philipp Hörist's avatar
Philipp Hörist committed
824
825
        contact.show = show
        self.add_contact(jid, account)
826
827
828

        return contact

Philipp Hörist's avatar
Philipp Hörist committed
829
    def remove_groupchat(self, jid, account, maximize=False):
830
831
832
        """
        Remove groupchat from roster and redraw account and group
        """
833
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
834
        if contact.is_groupchat:
835
836
            if jid in app.interface.minimized_controls[account]:
                del app.interface.minimized_controls[account][jid]
Philipp Hörist's avatar
Philipp Hörist committed
837
            self.remove_contact(jid, account, force=True, backend=True, maximize=maximize)
838
            return True
839
        return False
840
841
842
843
844
845

    # FIXME: This function is yet unused! Port to new API
    def add_transport(self, jid, account):
        """
        Add transport to roster and draw it. Return the added contact instance
        """
846
        contact = app.contacts.get_contact_with_highest_priority(account, jid)
847
        if contact is None:
848
            contact = app.contacts.create_contact(jid=jid, account=account,
849
850
                name=jid, groups=[_('Transports')], show='offline',
                status='offline', sub='from')
851
            app.contacts.add_contact(account, contact)
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
        self.add_contact(jid, account)
        return contact

    def remove_transport(self, jid, account):
        """
        Remove transport from roster and redraw account and group
        """
        self.remove_contact(jid, account, force=True, backend=True)
        return True

    def rename_group(self, old_name, new_name, account):
        """
        Rename a roster group
        """
        if old_name == new_name:
            return

        # Groups may not change name from or to a special groups
        for g in helpers.special_groups:
            if g in (new_name, old_name):
                return

        # update all contacts in the given group
        if self.regroup:
876
            accounts = app.connections.keys()
877
        else:
878
            accounts = [account, ]
879
880
881

        for acc in accounts:
            changed_contacts = []
882
883
            for jid in app.contacts.get_jid_list(acc):
                contact = app.contacts.get_first_contact_from_jid(acc, jid)
884
885
886
887
888
889
890
891
892
                if old_name not in contact.groups:
                    continue

                self.remove_contact(jid, acc, force=True)

                contact.groups.remove(old_name)
                if new_name not in contact.groups:
                    contact.groups.append(new_name)

893
894
                changed_contacts.append({'jid': jid, 'name': contact.name,
                    'groups':contact.groups})
895

896
897
            app.connections[acc].get_module('Roster').update_contacts(
                changed_contacts)
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921

            for c in changed_contacts:
                self.add_contact(c['jid'], acc)

            self._adjust_group_expand_collapse_state(new_name, acc)

            self.draw_group(old_name, acc)
            self.draw_group(new_name, acc)


    def add_contact_to_groups(self, jid, account, groups, update=True):
        """
        Add contact to given groups and redraw them

        Contact on server is updated too. When the contact has a family,
        the action will be performed for all members.

        Keyword Arguments:
        jid -- the jid
        account -- the corresponding account
        groups -- list of Groups to add the contact to.
        update -- update contact on the server
        """
        self.remove_contact(jid, account, force=True)
922
        for contact in app.contacts.get_contacts(account, jid):
923
924
925
926
927
            for group in groups:
                if group not in contact.groups:
                    # we might be dropped from meta to group
                    contact.groups.append(group)
            if update:
928
929
930
                con = app.connections[account]
                con.get_module('Roster').update_contact(
                    jid, contact.name, contact.groups)
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950

        self.add_contact(jid, account)

        for group in groups:
            self._adjust_group_expand_collapse_state(group, account)

    def remove_contact_from_groups(self, jid, account, groups, update=True):
        """
        Remove contact from given groups and redraw them

        Contact on server is updated too. When the contact has a family,
        the action will be performed for all members.

        Keyword Arguments:
        jid -- the jid
        account -- the corresponding account
        groups -- list of Groups to remove the contact from
        update -- update contact on the server
        """
        self.remove_contact(jid, account, force=True)
951
        for contact in app.contacts.get_contacts(account, jid):
952
953
954
955
956
            for group in groups:
                if group in contact.groups:
                    # Needed when we remove from "General" or "Observers"
                    contact.groups.remove(group)
            if update:
957
958
959
                con = app.connections[account]
                con.get_module('Roster').update_contact(
                    jid, contact.name, contact.groups)
960
961
962
963
964
965
        self.add_contact(jid, account)

        # Also redraw old groups
        for group in groups:
            self.draw_group(group, account)

966
    # FIXME: maybe move to app.py
967
    def remove_newly_added(self, jid, account):
968
        if account not in app.newly_added:
969
970
            # Account has been deleted during the timeout that called us
            return
971
972
        if jid in app.newly_added[account]:
            app.newly_added[account].remove(jid)
973
974
            self.draw_contact(jid, account)

975
    # FIXME: maybe move to app.py
976
    def remove_to_be_removed(self, jid, account):
977
        if account not in app.interface.instances:
978
979
            # Account has been deleted during the timeout that called us
            return
980
        if jid in app.newly_added[account]:
981
            return
982
983
984
        if jid in app.to_be_removed[account]:
            app.to_be_removed[account].remove(jid)
            family = app.contacts.get_metacontacts_family(account, jid)
985
            if family:
Alexander Krotov's avatar
Alexander Krotov committed
986
                # Perform delayed recalibration
987
988
                self._recalibrate_metacontact_family(family, account)
            self.draw_contact(jid, account)
Alexander Krotov's avatar
Alexander Krotov committed
989
            # Hide Group if all children are hidden
990
            contact = app.contacts.get_contact(account, jid)
Weblate's avatar
Weblate committed
991
992
            if not contact:
                return
993
994
            for group in contact.get_shown_groups():
                self.draw_group(group, account)
995
996

    # FIXME: integrate into add_contact()
Philipp Hörist's avatar
Philipp Hörist committed
997
998
999
1000
    def add_to_not_in_the_roster(self, account, jid, nick='', resource='',
                                 groupchat=False):
        contact = app.contacts.create_not_in_roster_contact(
            jid=jid, account=account, resource=resource, name=nick,
Philipp Hörist's avatar
Philipp Hörist committed
1001
            groupchat=groupchat)
1002
        app.contacts.add_contact(account, contact)
1003
1004
        self.add_contact(contact.jid, account)
        return contact
Yann Leboulanger's avatar
Yann Leboulanger committed
1005
1006
1007


################################################################################
1008
### Methods for adding and removing roster window items
Yann Leboulanger's avatar
Yann Leboulanger committed
1009
################################################################################
1010

1011
    def _really_draw_account(self, account):
1012
1013
1014
1015
        child_iter = self._get_account_iter(account, self.model)
        if not child_iter:
            return

1016
1017
        num_of_accounts = app.get_number_of_connected_accounts()
        num_of_secured = app.get_number_of_securely_connected_accounts()
1018

Philipp Hörist's avatar
Philipp Hörist committed
1019
        tls_pixbuf = None
1020
        if app.account_is_securely_connected(account) and not self.regroup or\
1021
        self.regroup and num_of_secured and num_of_secured == num_of_accounts:
Philipp Hörist's avatar
Philipp Hörist committed
1022
            tls_pixbuf = 'changes-prevent'
1023
            self.model[child_iter][Column.PADLOCK_PIXBUF] = tls_pixbuf
1024
1025
1026
1027
1028

        if self.regroup:
            account_name = _('Merged accounts')
            accounts = []
        else:
1029
            account_name = app.get_account_label(account)
1030
1031
1032
1033
1034
1035
            accounts = [account]

        if account in self.collapsed_rows and \
        self.model.iter_has_child(child_iter):
            account_name = '[%s]' % account_name

1036
1037
        if (app.account_is_connected(account) or (self.regroup and \
        app.get_number_of_connected_accounts())) and app.config.get(
1038
        'show_contacts_number'):
1039
            nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts(
1040
                    accounts=accounts)
1041
1042
            account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))

1043
        self.model[child_iter][Column.NAME] = GLib.markup_escape_text(account_name)
1044

1045
        pep_dict = app.connections[account].pep
1046
1047
        if app.config.get('show_mood_in_roster') and PEPEventType.MOOD in pep_dict:
            self.model[child_iter][Column.MOOD_PIXBUF] = 'mood-%s' % pep_dict[PEPEventType.MOOD].mood
1048
        else:
1049
            self.model[child_iter][Column.MOOD_PIXBUF] = None
1050

Philipp Hörist's avatar
Philipp Hörist committed
1051
1052
1053
1054
1055
        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
1056
        else:
1057
            self.model[child_iter][Column.ACTIVITY_PIXBUF] = None
1058

Philipp Hörist's avatar
Philipp Hörist committed
1059
1060
        if app.config.get('show_tunes_in_roster') and PEPEventType.TUNE in pep_dict:
            self.model[child_iter][Column.TUNE_ICON] = 'audio-x-generic'
1061
        else:
1062
            self.model[child_iter][Column.TUNE_ICON] = None
1063

Philipp Hörist's avatar
Philipp Hörist committed
1064
1065
        if app.config.get('show_location_in_roster') and PEPEventType.LOCATION in pep_dict:
            self.model[child_iter][Column.LOCATION_ICON] = 'applications-internet'
1066
        else:
1067
            self.model[child_iter][Column.LOCATION_ICON] = None
1068
1069
1070
1071
1072

    def _really_draw_accounts(self):
        for acct in self.accounts_to_draw:
            self._really_draw_account(acct)
        self.accounts_to_draw = []
1073
1074
        return False

1075
1076
1077
1078
1079
    def draw_account(self, account):
        if account in self.accounts_to_draw:
            return
        self.accounts_to_draw.append(account)
        if len(self.accounts_to_draw) == 1:
Yann Leboulanger's avatar
Yann Leboulanger committed
1080
            GLib.timeout_add(200, self._really_draw_accounts)
1081
1082

    def _really_draw_group(self, group, account):
1083
1084
        child_iter = self._get_group_iter(group, account, model=self.model)
        if not child_iter:
Alexander Krotov's avatar
Alexander Krotov committed
1085
            # Eg. We redraw groups after we removed a entity
1086
1087
1088
1089
1090
1091
            # and its empty groups
            return
        if self.regroup:
            accounts = []
        else:
            accounts = [account]
Yann Leboulanger's avatar
Yann Leboulanger committed
1092
        text = GLib.markup_escape_text(group)
1093
1094
        if helpers.group_is_blocked(account, group):
            text = '<span strikethrough="true">%s</span>' % text
1095
1096
        if app.config.get('show_contacts_number'):
            nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts(
1097
                    accounts=accounts, groups=[group])
1098
1099
            text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))

1100
        self.model[child_iter][Column.NAME] = text
1101

1102
1103
1104
        # Hide group if no more contacts
        iterG = self._get_group_iter(group, account, model=self.modelfilter)
        to_hide = []
1105
        while iterG:
1106
            parent = self.modelfilter.iter_parent(iterG)
1107
1108
            if (not self.modelfilter.iter_has_child(iterG)) or (to_hide \
            and self.modelfilter.iter_n_children(iterG) == 1):
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
                to_hide.append(iterG)
                if not parent or self.modelfilter[parent][Column.TYPE] != \
                'group':
                    iterG = None
                else:
                    iterG = parent
            else:
                iterG = None
        for iter_ in to_hide:
            self.modelfilter[iter_][Column.VISIBLE] = False

1120
    def _really_draw_groups(self):
Dicson's avatar
Dicson committed
1121
        for ag in self.groups_to_draw.values():
1122
1123
1124
1125
            acct = ag['account']
            grp = ag['group']
            self._really_draw_group(grp, acct)
        self.groups_to_draw = {}
1126
1127
        return False

1128
1129
1130
1131
1132
1133
    def draw_group(self, group, account):
        ag = account + group
        if ag in self.groups_to_draw:
            return
        self.groups_to_draw[ag] = {'group': group, 'account': account}
        if len(self.groups_to_draw) == 1:
Yann Leboulanger's avatar
Yann Leboulanger committed
1134
            GLib.timeout_add(200, self._really_draw_groups)
1135

1136
1137
1138