roster_window.py 271 KB
Newer Older
1
# -*- coding: utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2
## src/roster_window.py
3
##
Dicson's avatar
Dicson committed
4
## Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
5
## Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
roidelapluie's avatar
roidelapluie committed
6
##                    Stéphan Kochen <stephan AT kochen.nl>
roidelapluie's avatar
roidelapluie committed
7 8 9 10 11 12 13 14 15 16 17 18
## 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>
19
##
Yann Leboulanger's avatar
Yann Leboulanger committed
20 21 22
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
23
## it under the terms of the GNU General Public License as published
Yann Leboulanger's avatar
Yann Leboulanger committed
24
## by the Free Software Foundation; version 3 only.
25
##
Yann Leboulanger's avatar
Yann Leboulanger committed
26
## Gajim is distributed in the hope that it will be useful,
27
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
28
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 30
## GNU General Public License for more details.
##
Yann Leboulanger's avatar
Yann Leboulanger committed
31
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
32
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
Yann Leboulanger's avatar
Yann Leboulanger committed
33
##
34

35 36 37 38 39
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import Pango
from gi.repository import GObject
Yann Leboulanger's avatar
Yann Leboulanger committed
40
from gi.repository import GLib
41
import os
Yann Leboulanger's avatar
Yann Leboulanger committed
42
import sys
43
import time
44
import locale
45

46
import common.sleepy
47 48
import history_window
import dialogs
nkour's avatar
nkour committed
49
import vcard
50
import config
51
import disco
dkirov's avatar
dkirov committed
52
import gtkgui_helpers
53
import gui_menu_builder
54
import cell_renderer_image
55
import tooltips
nicfit's avatar
nicfit committed
56
import message_control
Liorithiel's avatar
Liorithiel committed
57
import adhoc_commands
58
import features_window
59
import shortcuts_window
60 61
import plugins
import plugins.gui
62 63 64

from common import gajim
from common import helpers
65
from common.exceptions import GajimGeneralException
66
from common import i18n
67
from common import pep
68
from common import location_listener
69
from common import ged
70

71
from message_window import MessageWindowMgr
72

73 74
from common import dbus_support
if dbus_support.supported:
75
    import dbus
Yann Leboulanger's avatar
Yann Leboulanger committed
76

77
from nbxmpp.protocol import NS_FILE, NS_ROSTERX, NS_CONFERENCE
78

79
#(icon, name, type, jid, account, editable, second pixbuf)
80
(
81 82 83 84 85 86 87 88 89 90 91
    C_IMG, # image to show state (online, new message etc)
    C_NAME, # cellrenderer text that holds contact nickame
    C_TYPE, # account, group or contact?
    C_JID, # the jid of the row
    C_ACCOUNT, # cellrenderer text that holds account name
    C_MOOD_PIXBUF,
    C_ACTIVITY_PIXBUF,
    C_TUNE_PIXBUF,
    C_LOCATION_PIXBUF,
    C_AVATAR_PIXBUF, # avatar_pixbuf
    C_PADLOCK_PIXBUF, # use for account row only
92
) = range(11)
93

Dicson's avatar
Dicson committed
94
empty_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 1, 1)
Dicson's avatar
Dicson committed
95
empty_pixbuf.fill(0xffffff00)
Dicson's avatar
Dicson committed
96 97


98
class RosterWindow:
99 100 101 102 103 104
    """
    Class for main window of the GTK+ interface
    """

    def _get_account_iter(self, name, model=None):
        """
105
        Return the Gtk.TreeIter of the given account or None if not found
106 107 108 109 110 111

        Keyword arguments:
        name -- the account name
        model -- the data model (default TreeFilterModel)
        """
        if model is None:
112 113 114 115
            model = self.modelfilter
            if model is None:
                return

116
        if self.regroup:
117
            name = 'MERGED'
118 119
        if name not in self._iters:
            return None
120
        it = self._iters[name]['account']
121 122 123 124

        if model == self.model or it is None:
            return it
        try:
125 126 127 128
            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
            if ok:
                return it
            return None
129 130
        except RuntimeError:
            return None
131 132


133
    def _get_group_iter(self, name, account, model=None):
134
        """
135
        Return the Gtk.TreeIter of the given group or None if not found
136 137 138 139 140 141

        Keyword arguments:
        name -- the group name
        account -- the account name
        model -- the data model (default TreeFilterModel)
        """
142
        if model is None:
143
            model = self.modelfilter
144 145 146
            if model is None:
                return

147 148 149
        if self.regroup:
            account = 'MERGED'

150 151
        if account not in self._iters:
            return None
152 153 154 155 156 157 158
        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:
159 160 161 162
            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
            if ok:
                return it
            return None
163 164
        except RuntimeError:
            return None
165 166 167 168


    def _get_self_contact_iter(self, account, model=None):
        """
169
        Return the Gtk.TreeIter of SelfContact or None if not found
170 171 172 173 174

        Keyword arguments:
        account -- the account of SelfContact
        model -- the data model (default TreeFilterModel)
        """
175 176 177 178
        jid = gajim.get_jid_from_account(account)
        its = self._get_contact_iter(jid, account, model=model)
        if its:
            return its[0]
179 180 181 182 183
        return None


    def _get_contact_iter(self, jid, account, contact=None, model=None):
        """
184
        Return a list of Gtk.TreeIter of the given contact
185 186 187 188 189 190 191

        Keyword arguments:
        jid -- the jid without resource
        account -- the account
        contact -- the contact (default None)
        model -- the data model (default TreeFilterModel)
        """
192
        if model is None:
193 194 195 196 197 198 199 200 201 202 203
            model = self.modelfilter
            # when closing Gajim model can be none (async pbs?)
            if model is None:
                return []

        if not contact:
            contact = gajim.contacts.get_first_contact_from_jid(account, jid)
            if not contact:
                # We don't know this contact
                return []

204 205 206
        if account not in self._iters:
            return []

207 208 209 210 211 212 213 214 215 216 217 218 219 220
        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:
221 222 223
                (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
                if ok:
                    its2.append(it)
224 225 226
            except RuntimeError:
                pass
        return its2
227 228


229
    def _iter_is_separator(self, model, titer, dummy):
230 231 232 233 234
        """
        Return True if the given iter is a separator

        Keyword arguments:
        model -- the data model
235
        iter -- the Gtk.TreeIter to test
236 237 238 239 240 241
        """
        if model[titer][0] == 'SEPARATOR':
            return True
        return False


Yann Leboulanger's avatar
Yann Leboulanger committed
242
#############################################################################
243
### Methods for adding and removing roster window items
steve-e's avatar
steve-e committed
244
#############################################################################
Yann Leboulanger's avatar
Yann Leboulanger committed
245

246 247 248 249 250 251 252 253 254 255 256
    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()
257 258 259
            it = self.model.append(None, [
                gajim.interface.jabber_state_images['16'][show],
                _('Merged accounts'), 'account', '', 'all', None, None, None,
260
                None, None, None] + [None] * self.nb_ext_renderers)
261
            self._iters['MERGED']['account'] = it
262 263 264 265 266 267
        else:
            show = gajim.SHOW_LIST[gajim.connections[account].connected]
            our_jid = gajim.get_jid_from_account(account)

            tls_pixbuf = None
            if gajim.account_is_securely_connected(account):
268
                tls_pixbuf = gtkgui_helpers.get_icon_pixmap('changes-prevent', 16)
269
                # the only way to create a pixbuf from stock
Yann Leboulanger's avatar
Yann Leboulanger committed
270 271
#                tls_pixbuf = self.window.render_icon_pixbuf(
#                    Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU)
272

273 274
            it = self.model.append(None, [
                gajim.interface.jabber_state_images['16'][show],
Yann Leboulanger's avatar
Yann Leboulanger committed
275
                GLib.markup_escape_text(account), 'account', our_jid,
276 277
                account, None, None, None, None, None, tls_pixbuf] +
                [None] * self.nb_ext_renderers)
278
            self._iters[account]['account'] = it
279 280 281 282

        self.draw_account(account)


283 284
    def add_account_contacts(self, account, improve_speed=True,
    draw_contacts=True):
285
        """
286 287
        Add all contacts and groups of the given account to roster, draw them
        and account
288
        """
289 290
        if improve_speed:
            self._before_fill()
291 292 293 294 295
        jids = gajim.contacts.get_jid_list(account)

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

296 297 298 299 300
        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)
301

302 303 304 305
            # Draw all known groups
            for group in gajim.groups[account]:
                self.draw_group(group, account)
            self.draw_account(account)
306

307 308
        if improve_speed:
            self._after_fill()
309

310 311 312 313 314 315 316 317 318 319 320
    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
        delimiter = gajim.connections[account].nested_group_delimiter
        group_splited = group.split(delimiter)
        parent_group = delimiter.join(group_splited[:-1])
321
        if len(group_splited) > 1 and parent_group in self._iters[account_group]['groups']:
322 323 324 325 326 327 328 329 330 331 332 333 334
            iter_parent = self._iters[account_group]['groups'][parent_group]
        elif parent_group:
            iter_parent = self._add_group_iter(account, parent_group)
            if parent_group not in gajim.groups[account]:
                if account + parent_group in self.collapsed_rows:
                    is_expanded = False
                else:
                    is_expanded = True
                gajim.groups[account][parent_group] = {'expand': is_expanded}
        else:
            iter_parent = self._get_account_iter(account, self.model)
        iter_group = self.model.append(iter_parent,
            [gajim.interface.jabber_state_images['16']['closed'],
Yann Leboulanger's avatar
Yann Leboulanger committed
335
            GLib.markup_escape_text(group), 'group', group, account, None,
336 337 338 339
            None, None, None, None, None] + [None] * self.nb_ext_renderers)
        self.draw_group(group, account)
        self._iters[account_group]['groups'][group] = iter_group
        return iter_group
340 341

    def _add_entity(self, contact, account, groups=None,
342
    big_brother_contact=None, big_brother_account=None):
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
        """
        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 = []
        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)
            assert len(parent_iters) > 0, 'Big brother is not yet in roster!'

            # Do not confuse get_contact_iter: Sync groups of family members
368
            contact.groups = big_brother_contact.groups[:]
369 370

            for child_iter in parent_iters:
371
                it = self.model.append(child_iter, [None,
372
                    contact.get_shown_name(), 'contact', contact.jid, account,
373 374
                    None, None, None, None, None, None] + \
                    [None] * self.nb_ext_renderers)
375
                added_iters.append(it)
376 377 378 379
                if contact.jid in self._iters[account]['contacts']:
                    self._iters[account]['contacts'][contact.jid].append(it)
                else:
                    self._iters[account]['contacts'][contact.jid] = [it]
380 381 382 383 384 385
        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,
386
                    model=self.model)
387 388
                if not child_iterG:
                    # Group is not yet in roster, add it!
389
                    child_iterG = self._add_group_iter(account, group)
390 391 392 393 394 395 396 397 398 399

                if contact.is_transport():
                    typestr = 'agent'
                elif contact.is_groupchat():
                    typestr = 'groupchat'
                else:
                    typestr = 'contact'

                # we add some values here. see draw_contact
                # for more
400 401 402 403
                i_ = self.model.append(child_iterG, [None,
                    contact.get_shown_name(), typestr, contact.jid, account,
                    None, None, None, None, None, None] + \
                    [None] * self.nb_ext_renderers)
404
                added_iters.append(i_)
405 406 407 408
                if contact.jid in self._iters[account]['contacts']:
                    self._iters[account]['contacts'][contact.jid].append(i_)
                else:
                    self._iters[account]['contacts'][contact.jid] = [i_]
409 410 411 412 413 414 415 416 417

                # Restore the group expand state
                if account + group in self.collapsed_rows:
                    is_expanded = False
                else:
                    is_expanded = True
                if group not in gajim.groups[account]:
                    gajim.groups[account][group] = {'expand': is_expanded}

418 419
        assert len(added_iters), '%s has not been added to roster!' % \
        contact.jid
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
        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.
        """
436 437
        iters = self._get_contact_iter(contact.jid, account, contact,
            self.model)
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
        assert iters, '%s shall be removed but is not in roster' % contact.jid

        parent_iter = self.model.iter_parent(iters[0])
        parent_type = self.model[parent_iter][C_TYPE]

        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
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
        # Remove us and empty groups from the model
        for i in iters:
            assert self.model[i][C_JID] == contact.jid and \
                    self.model[i][C_ACCOUNT] == account, \
                    "Invalidated iters of %s" % contact.jid

            parent_i = self.model.iter_parent(i)
            parent_type = self.model[parent_i][C_TYPE]

            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
472
                group = self.model[parent_i][C_JID]
Yann Leboulanger's avatar
Yann Leboulanger committed
473 474 475 476 477
                if group in gajim.groups[account]:
                    del gajim.groups[account][group]
                to_be_removed = parent_i
                del self._iters[account_group]['groups'][group]
                parent_i = self.model.iter_parent(parent_i)
478
                parent_type = self.model[parent_i][C_TYPE]
Yann Leboulanger's avatar
Yann Leboulanger committed
479
            self.model.remove(to_be_removed)
480

Yann Leboulanger's avatar
Yann Leboulanger committed
481 482
        del self._iters[account]['contacts'][contact.jid]
        return True
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497

    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)
498 499
        if not big_brother_jid:
            return []
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        big_brother_contact = gajim.contacts.get_first_contact_from_jid(
                big_brother_account, big_brother_jid)

        assert len(self._get_contact_iter(big_brother_jid,
                big_brother_account, big_brother_contact, self.model)) == 0, \
                'Big brother %s already in roster\n Family: %s' \
                % (big_brother_jid, family)
        self._add_entity(big_brother_contact, big_brother_account)

        brothers = []
        # Filter family members
        for data in nearby_family:
            _account = data['account']
            _jid = data['jid']
            _contact = gajim.contacts.get_first_contact_from_jid(
                    _account, _jid)

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

            assert len(self._get_contact_iter(_jid, _account,
                    _contact, self.model)) == 0, \
                    "%s already in roster.\n Family: %s" % (_jid, nearby_family)
            self._add_entity(_contact, _account,
                    big_brother_contact = big_brother_contact,
                    big_brother_account = big_brother_account)
            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).
        # Remove childs first then big brother
        family_in_roster = False
        for data in nearby_family:
            _account = data['account']
            _jid = data['jid']
            _contact = gajim.contacts.get_first_contact_from_jid(_account, _jid)

            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
            assert iters, '%s shall be removed but is not in roster \
                    \n Family: %s' % (_jid, family)

            family_in_roster = True

            parent_iter = self.model.iter_parent(iters[0])
            parent_type = self.model[parent_iter][C_TYPE]

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

            ok = self._remove_entity(_contact, _account)
            assert ok, '%s was not removed' % _jid
            assert len(self._get_contact_iter(_jid, _account, _contact,
574
                self.model)) == 0, '%s is removed but still in roster' % _jid
575 576 577 578

        if not family_in_roster:
            return False

Yann Leboulanger's avatar
Yann Leboulanger committed
579
        assert old_big_jid, 'No Big Brother in nearby family %s (Family: %s)' %\
580
            (nearby_family, family)
581
        iters = self._get_contact_iter(old_big_jid, old_big_account,
582
            old_big_contact, self.model)
583
        assert len(iters) > 0, 'Old Big Brother %s is not in roster anymore' % \
584 585 586
            old_big_jid
        assert not self.model.iter_children(iters[0]), \
            'Old Big Brother %s still has children' % old_big_jid
587 588 589 590

        ok = self._remove_entity(old_big_contact, old_big_account)
        assert ok, "Old Big Brother %s not removed" % old_big_jid
        assert len(self._get_contact_iter(old_big_jid, old_big_account,
591 592
            old_big_contact, self.model)) == 0, \
            'Old Big Brother %s is removed but still in roster' % old_big_jid
593 594 595 596 597 598 599 600 601 602

        return True

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

        brothers = []
        nearby_family, big_brother_jid, big_brother_account = \
603
            self._get_nearby_family_and_big_brother(family, account)
604
        big_brother_contact = gajim.contacts.get_contact(big_brother_account,
605 606 607
            big_brother_jid)
        child_iters = self._get_contact_iter(big_brother_jid,
            big_brother_account, model=self.model)
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
        if child_iters:
            parent_iter = self.model.iter_parent(child_iters[0])
            parent_type = self.model[parent_iter][C_TYPE]

            # Check if the current BigBrother has even been before.
            if parent_type == 'contact':
                for data in nearby_family:
                    # recalibrate after remove to keep highlight
                    if data['jid'] in gajim.to_be_removed[data['account']]:
                        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
631 632
            child_iters = self._get_contact_iter(_jid, _account,
                model=self.model)
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
            if not child_iters:
                continue
            parent_iter = self.model.iter_parent(child_iters[0])
            parent_type = self.model[parent_iter][C_TYPE]
            if parent_type != 'contact':
                _contact = gajim.contacts.get_contact(_account, _jid)
                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):
        return gajim.contacts.get_nearby_family_and_big_brother(family, account)

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

        Return the SelfContact contact instance
        """
        jid = gajim.get_jid_from_account(account)
        contact = gajim.contacts.get_first_contact_from_jid(account, jid)

656 657
        assert len(self._get_contact_iter(jid, account, contact,
        self.model)) == 0, 'Self contact %s already in roster' % jid
658 659

        child_iterA = self._get_account_iter(account, self.model)
660
        self._iters[account]['contacts'][jid] = [self.model.append(child_iterA,
661 662
            [None, gajim.nicks[account], 'self_contact', jid, account, None,
            None, None, None, None, None] + [None] * self.nb_ext_renderers)]
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696

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

        return contact

    def redraw_metacontacts(self, account):
        for family in gajim.contacts.iter_metacontacts_families(account):
            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.
        """
        contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
        if len(self._get_contact_iter(jid, account, contact, self.model)):
            # If contact already in roster, do nothing
            return

        if jid == gajim.get_jid_from_account(account):
            show_self_contact = gajim.config.get('show_self_contact')
            if show_self_contact == 'never':
                return
697 698 699
            if (contact.resource != gajim.connections[account].server_resource \
            and show_self_contact == 'when_other_resource') or \
            show_self_contact == 'always':
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
                return self._add_self_contact(account)
            return

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

        # Add contact to roster
        family = gajim.contacts.get_metacontacts_family(account, jid)
        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
720
            contacts = [(contact, account), ]
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
            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

    def remove_contact(self, jid, account, force=False, backend=False):
        """
        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)
        """
        contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
        if not contact:
            return

754 755 756
        if not force and self.contact_has_pending_roster_events(contact,
        account):
            return False
757 758 759 760 761 762 763 764 765 766 767 768

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

769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
        old_grps = []
        if backend:
            if not gajim.interface.msg_win_mgr.get_control(jid, account) or \
            force:
                # If a window is still opened: don't remove contact instance
                # Remove contact before redrawing, otherwise the old
                # numbers will still be show
                gajim.contacts.remove_jid(account, jid, remove_meta=True)
                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:
                for c in gajim.contacts.get_contacts(account, jid):
                    c.sub = 'none'
                    c.show = 'not in roster'
                    c.status = ''
                    old_grps = c.get_shown_groups()
                    c.groups = [_('Not in Roster')]
                    self._add_entity(c, account)
                    self.draw_contact(jid, account)
795 796 797

        if iters:
            # Draw all groups of the contact
798
            for group in contact.get_shown_groups() + old_grps:
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
                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.
        """
        gajim.contacts.change_contact_jid(old_jid, new_jid, account)
        self_iter = self._get_self_contact_iter(account, model=self.model)
        if not self_iter:
            return
        self.model[self_iter][C_JID] = new_jid
        self.draw_contact(new_jid, account)

    def add_groupchat(self, jid, account, status=''):
        """
        Add groupchat to roster and draw it. Return the added contact instance
        """
        contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
        # Do not show gc if we are disconnected and minimize it
        if gajim.account_is_connected(account):
            show = 'online'
        else:
            show = 'offline'
            status = ''

        if contact is None:
833 834
            gc_control = gajim.interface.msg_win_mgr.get_gc_control(jid,
                account)
835 836 837 838 839 840 841 842 843
            if gc_control:
                # there is a window that we can minimize
                gajim.interface.minimized_controls[account][jid] = gc_control
                name = gc_control.name
            elif jid in gajim.interface.minimized_controls[account]:
                name = gajim.interface.minimized_controls[account][jid].name
            else:
                name = jid.split('@')[0]
            # New groupchat
844 845 846
            contact = gajim.contacts.create_contact(jid=jid, account=account,
                name=name, groups=[_('Groupchats')], show=show, status=status,
                sub='none')
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
            gajim.contacts.add_contact(account, contact)
            self.add_contact(jid, account)
        else:
            if jid not in gajim.interface.minimized_controls[account]:
                # there is a window that we can minimize
                gc_control = gajim.interface.msg_win_mgr.get_gc_control(jid,
                        account)
                gajim.interface.minimized_controls[account][jid] = gc_control
            contact.show = show
            contact.status = status
            self.adjust_and_draw_contact_context(jid, account)

        return contact


    def remove_groupchat(self, jid, account):
        """
        Remove groupchat from roster and redraw account and group
        """
        contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
        if contact.is_groupchat():
            if jid in gajim.interface.minimized_controls[account]:
                del gajim.interface.minimized_controls[account][jid]
            self.remove_contact(jid, account, force=True, backend=True)
            return True
        else:
            return False


    # 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
        """
        contact = gajim.contacts.get_contact_with_highest_priority(account, jid)
        if contact is None:
883 884 885
            contact = gajim.contacts.create_contact(jid=jid, account=account,
                name=jid, groups=[_('Transports')], show='offline',
                status='offline', sub='from')
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
            gajim.contacts.add_contact(account, contact)
        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:
Dicson's avatar
Dicson committed
911
            accounts = gajim.connections.keys()
912
        else:
913
            accounts = [account, ]
914 915 916 917 918 919 920 921 922 923 924 925 926 927

        for acc in accounts:
            changed_contacts = []
            for jid in gajim.contacts.get_jid_list(acc):
                contact = gajim.contacts.get_first_contact_from_jid(acc, jid)
                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)

928 929
                changed_contacts.append({'jid': jid, 'name': contact.name,
                    'groups':contact.groups})
930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999

            gajim.connections[acc].update_contacts(changed_contacts)

            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)
        for contact in gajim.contacts.get_contacts(account, jid):
            for group in groups:
                if group not in contact.groups:
                    # we might be dropped from meta to group
                    contact.groups.append(group)
            if update:
                gajim.connections[account].update_contact(jid, contact.name,
                        contact.groups)

        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)
        for contact in gajim.contacts.get_contacts(account, jid):
            for group in groups:
                if group in contact.groups:
                    # Needed when we remove from "General" or "Observers"
                    contact.groups.remove(group)
            if update:
                gajim.connections[account].update_contact(jid, contact.name,
                        contact.groups)
        self.add_contact(jid, account)

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

    # FIXME: maybe move to gajim.py
    def remove_newly_added(self, jid, account):
1000 1001 1002
        if account not in gajim.newly_added:
            # Account has been deleted during the timeout that called us
            return
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
        if jid in gajim.newly_added[account]:
            gajim.newly_added[account].remove(jid)
            self.draw_contact(jid, account)

    # FIXME: maybe move to gajim.py
    def remove_to_be_removed(self, jid, account):
        if account not in gajim.interface.instances:
            # Account has been deleted during the timeout that called us
            return
        if jid in gajim.newly_added[account]:
            return
        if jid in gajim.to_be_removed[account]:
            gajim.to_be_removed[account].remove(jid)
            family = gajim.contacts.get_metacontacts_family(account, jid)
            if family:
                # Peform delayed recalibration
                self._recalibrate_metacontact_family(family, account)
            self.draw_contact(jid, account)

    # FIXME: integrate into add_contact()
    def add_to_not_in_the_roster(self, account, jid, nick='', resource=''):
        keyID = ''
        attached_keys = gajim.config.get_per('accounts', account,
                'attached_gpg_keys').split()
        if jid in attached_keys:
            keyID = attached_keys[attached_keys.index(jid) + 1]
        contact = gajim.contacts.create_not_in_roster_contact(jid=jid,
                account=account, resource=resource, name=nick, keyID=keyID)
        gajim.contacts.add_contact(account, contact)
        self.add_contact(contact.jid, account)
        return contact
Yann Leboulanger's avatar
Yann Leboulanger committed
1034 1035 1036


################################################################################
1037
### Methods for adding and removing roster window items
Yann Leboulanger's avatar
Yann Leboulanger committed
1038
################################################################################
1039

1040
    def _really_draw_account(self, account):
1041 1042 1043 1044 1045 1046 1047 1048
        child_iter = self._get_account_iter(account, self.model)
        if not child_iter:
            assert False, 'Account iter of %s could not be found.' % account
            return

        num_of_accounts = gajim.get_number_of_connected_accounts()
        num_of_secured = gajim.get_number_of_securely_connected_accounts()

1049
        if gajim.account_is_securely_connected(account) and not self.regroup or\
1050
        self.regroup and num_of_secured and num_of_secured == num_of_accounts:
1051
            tls_pixbuf = gtkgui_helpers.get_icon_pixmap('changes-prevent', 16)
1052
            # the only way to create a pixbuf from stock
Yann Leboulanger's avatar
Yann Leboulanger committed
1053 1054
#            tls_pixbuf = self.window.render_icon_pixbuf(
#                Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU)
1055 1056
            self.model[child_iter][C_PADLOCK_PIXBUF] = tls_pixbuf
        else:
Dicson's avatar
Dicson committed
1057
            self.model[child_iter][C_PADLOCK_PIXBUF] = empty_pixbuf
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078

        if self.regroup:
            account_name = _('Merged accounts')
            accounts = []
        else:
            account_name = account
            accounts = [account]

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

        if (gajim.account_is_connected(account) or (self.regroup and \
        gajim.get_number_of_connected_accounts())) and gajim.config.get(
        'show_contacts_number'):
            nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts(
                    accounts = accounts)
            account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))

        self.model[child_iter][C_NAME] = account_name

1079 1080
        pep_dict = gajim.connections[account].pep
        if gajim.config.get('show_mood_in_roster') and 'mood' in pep_dict:
1081 1082
            self.model[child_iter][C_MOOD_PIXBUF] = \
                gtkgui_helpers.get_pep_as_pixbuf(pep_dict['mood'])
1083
        else:
Dicson's avatar
Dicson committed
1084
            self.model[child_iter][C_MOOD_PIXBUF] = empty_pixbuf
1085

1086 1087
        if gajim.config.get('show_activity_in_roster') and 'activity' in \
        pep_dict:
1088 1089
            self.model[child_iter][C_ACTIVITY_PIXBUF] = \
                gtkgui_helpers.get_pep_as_pixbuf(pep_dict['activity'])
1090
        else:
Dicson's avatar
Dicson committed
1091
            self.model[child_iter][C_ACTIVITY_PIXBUF] = empty_pixbuf
1092

1093
        if gajim.config.get('show_tunes_in_roster') and 'tune' in pep_dict:
1094 1095
            self.model[child_iter][C_TUNE_PIXBUF] = \
                gtkgui_helpers.get_pep_as_pixbuf(pep_dict['tune'])
1096
        else:
Dicson's avatar
Dicson committed
1097
            self.model[child_iter][C_TUNE_PIXBUF] = empty_pixbuf
1098

1099 1100
        if gajim.config.get('show_location_in_roster') and 'location' in \
        pep_dict:
1101 1102
            self.model[child_iter][C_LOCATION_PIXBUF] = \
                gtkgui_helpers.get_pep_as_pixbuf(pep_dict['location'])
1103
        else:
Dicson's avatar
Dicson committed
1104
            self.model[child_iter][C_LOCATION_PIXBUF] = empty_pixbuf
1105 1106 1107 1108 1109

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

1112 1113 1114 1115 1116
    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
1117
            GLib.timeout_add(200, self._really_draw_accounts)
1118 1119

    def _really_draw_group(self, group, account):
1120 1121 1122 1123 1124 1125 1126 1127 1128
        child_iter = self._get_group_iter(group, account, model=self.model)
        if not child_iter:
            # Eg. We redraw groups after we removed a entitiy
            # and its empty groups
            return
        if self.regroup:
            accounts = []
        else:
            accounts = [account]
Yann Leboulanger's avatar
Yann Leboulanger committed
1129
        text = GLib.markup_escape_text(group)
1130 1131 1132 1133 1134 1135 1136 1137
        if helpers.group_is_blocked(account, group):
            text = '<span strikethrough="true">%s</span>' % text
        if gajim.config.get('show_contacts_number'):
            nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts(
                    accounts = accounts, groups = [group])
            text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))

        self.model[child_iter][C_NAME] = text
1138 1139

    def _really_draw_groups(self):
Dicson's avatar
Dicson committed
1140
        for ag in self.groups_to_draw.values():
1141 1142 1143 1144
            acct = ag['account']
            grp = ag['group']
            self._really_draw_group(grp, acct)
        self.groups_to_draw = {}
1145 1146
        return False

1147 1148 1149 1150 1151 1152
    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
1153
            GLib.timeout_add(200, self._really_draw_groups)
1154

1155 1156 1157 1158 1159 1160 1161 1162
    def draw_parent_contact(self, jid, account):
        child_iters = self._get_contact_iter(jid, account, model=self.model)
        if not child_iters:
            return False
        parent_iter = self.model.iter_parent(child_iters[0])
        if self.model[parent_iter][C_TYPE] != 'contact':
            # parent is not a contact
            return
1163 1164
        parent_jid = self.model[parent_iter][C_JID]
        parent_account = self.model[parent_iter][C_ACCOUNT]
1165 1166 1167
        self.draw_contact(parent_jid, parent_account)
        return False

1168 1169
    def draw_contact(self, jid, account, selected=False, focus=False,
    contact_instances=None, contact=None):
1170 1171 1172 1173 1174 1175
        """
        Draw the correct state image, name BUT not avatar
        """
        # focus is about if the roster window has toplevel-focus or not
        # FIXME: We really need a custom cell_renderer

1176 1177 1178 1179 1180
        if not contact_instances:
            contact_instances = gajim.contacts.get_contacts(account, jid)
        if not contact:
            contact = gajim.contacts.get_highest_prio_contact_from_contacts(
                contact_instances)
1181 1182
        if not contact:
            return False
1183 1184 1185 1186 1187

        child_iters = self._get_contact_iter(jid, account, contact, self.model)
        if not child_iters:
            return False

Yann Leboulanger's avatar
Yann Leboulanger committed
1188
        name = GLib.markup_escape_text(contact.get_shown_name())
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221

        # gets number of unread gc marked messages
        if jid in gajim.interface.minimized_controls[account] and \
        gajim.interface.minimized_controls[account][jid]:
            nb_unread = len(gajim.events.get_events(account, jid,
                    ['printed_marked_gc_msg']))
            nb_unread += gajim.interface.minimized_controls \
                    [account][jid].get_nb_unread_pm()

            if nb_unread == 1:
                name = '%s *' % name
            elif nb_unread > 1:
                name = '%s [%s]' % (name, str(nb_unread))

        # Strike name if blocked
        strike = False
        if helpers.jid_is_blocked(account, jid):
            strike = True
        else:
            for group in contact.get_shown_groups():
                if helpers.group_is_blocked(account, group):
                    strike = True
                    break
        if strike:
            name = '<span strikethrough="true">%s</span>' % name

        # Show resource counter
        nb_connected_contact = 0
        for c in contact_instances:
            if c.show not in ('error', 'offline'):
                nb_connected_contact += 1
        if nb_connected_contact > 1:
            # switch back to default writing direction
Yann Leboulanger's avatar
Yann Leboulanger committed
1222
            name += i18n.paragraph_direction_mark(name)
1223
            name += ' (%d)' % nb_connected_contact
1224 1225 1226 1227

        # add status msg, if not empty, under contact name in
        # the treeview
        if contact.status and gajim.config.get('show_status_msgs_in_roster'):
1228
            status = contact.status.strip()
Dicson's avatar
Dicson committed
1229 1230 1231
            if status != '':
                status = helpers.reduce_chars_newlines(status,
                    max_lines = 1)
1232 1233 1234
                # escape markup entities and make them small
                # italic and fg color color is calcuted to be
                # always readable
1235 1236
                color = gtkgui_helpers.get_fade_color(self.tree, selected,
                    focus)
Yann Leboulanger's avatar
Yann Leboulanger committed
1237 1238
                colorstring = '#%04x%04x%04x' % (int(color.red * 65535),
                    int(color.green * 65535), int(color.blue * 65535))
1239
                name += '\n<span size="small" style="italic" ' \
1240
                    'foreground="%s">%s</span>' % (colorstring,
Yann Leboulanger's avatar
Yann Leboulanger committed
1241
                    GLib.markup_escape_text(status))
1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256

        icon_name = helpers.get_icon_name_to_show(contact, account)
        # look if another resource has awaiting events
        for c in contact_instances:
            c_icon_name = helpers.get_icon_name_to_show(c, account)
            if c_icon_name in ('event', 'muc_active', 'muc_inactive'):
                icon_name = c_icon_name
                break

        # Check for events of collapsed (hidden) brothers
        family = gajim.contacts.get_metacontacts_family(account, jid)
        is_big_brother = False
        have_visible_children = False
        if family:
            bb_jid, bb_account = \
1257
                self._get_nearby_family_and_big_brother(family, account)[1:]
1258 1259
            is_big_brother = (jid, account) == (bb_jid, bb_account)
            iters = self._get_contact_iter(jid, account)
Dicson's avatar
Dicson committed
1260 1261
            have_visible_children = iters and \
                self.modelfilter.iter_has_child(iters[0])
1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275

        if have_visible_children:
            # We are the big brother and have a visible family
            for child_iter in child_iters:
                child_path = self.model.get_path(child_iter)
                path = self.modelfilter.convert_child_path_to_path(child_path)

                if not path:
                    continue

                if not self.tree.row_expanded(path) and icon_name != 'event':
                    iterC = self.model.iter_children(child_iter)
                    while iterC:
                        # a child has awaiting messages?
1276 1277
                        jidC = self.model[iterC][C_JID]
                        accountC = self.model[iterC][C_ACCOUNT]
1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311
                        if len(gajim.events.get_events(accountC, jidC)):
                            icon_name = 'event'
                            break
                        iterC = self.model.iter_next(iterC)

                if self.tree.row_expanded(path):
                    state_images = self.get_appropriate_state_images(
                            jid, size = 'opened',
                            icon_name = icon_name)
                else:
                    state_images = self.get_appropriate_state_images(
                            jid, size = 'closed',
                            icon_name = icon_name)

                # Expand/collapse icon might differ per iter
                # (group)
                img = state_images[icon_name]
                self.model[child_iter][C_IMG] = img
                self.model[child_iter][C_NAME] = name
        else:
            # A normal contact or little brother
            state_images = self.get_appropriate_state_images(jid,
                    icon_name = icon_name)

            # All iters have the same icon (no expand/collapse)
            img = state_images[icon_name]
            for child_iter in child_iters:
                self<