1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
2# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
3#                    Stéphan Kochen <stephan AT kochen.nl>
4# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
5# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
6#                         Nikos Kouremenos <kourem AT gmail.com>
7# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
8# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
9# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
10#                    James Newton <redshodan AT gmail.com>
11#                    Tomasz Melcer <liori AT exroot.org>
12#                    Julien Pivotto <roidelapluie AT gmail.com>
13# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
14# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
15#                    Jonathan Schleifer <js-gajim AT webkeks.org>
16#
17# This file is part of Gajim.
18#
19# Gajim is free software; you can redistribute it and/or modify
20# it under the terms of the GNU General Public License as published
21# by the Free Software Foundation; version 3 only.
22#
23# Gajim is distributed in the hope that it will be useful,
24# but WITHOUT ANY WARRANTY; without even the implied warranty of
25# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26# GNU General Public License for more details.
27#
28# You should have received a copy of the GNU General Public License
29# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
30
31import os
32import sys
33import time
34import locale
35import logging
36from enum import IntEnum, unique
37
38from gi.repository import Gtk
39from gi.repository import Gdk
40from gi.repository import Pango
41from gi.repository import GObject
42from gi.repository import GLib
43from gi.repository import Gio
44from nbxmpp.namespaces import Namespace
45
46from gajim import dialogs
47from gajim import vcard
48from gajim import gtkgui_helpers
49from gajim import gui_menu_builder
50
51from gajim.common import app
52from gajim.common import helpers
53from gajim.common.exceptions import GajimGeneralException
54from gajim.common import i18n
55from gajim.common.helpers import save_roster_position
56from gajim.common.helpers import ask_for_status_message
57from gajim.common.i18n import _
58from gajim.common.const import PEPEventType, AvatarSize, StyleAttr
59from gajim.common.dbus import location
60
61from gajim.common import ged
62from gajim.message_window import MessageWindowMgr
63
64from gajim.gui.dialogs import DialogButton
65from gajim.gui.dialogs import ConfirmationDialog
66from gajim.gui.dialogs import ConfirmationCheckDialog
67from gajim.gui.dialogs import ErrorDialog
68from gajim.gui.dialogs import InputDialog
69from gajim.gui.dialogs import InformationDialog
70from gajim.gui.single_message import SingleMessageWindow
71from gajim.gui.add_contact import AddNewContactWindow
72from gajim.gui.service_registration import ServiceRegistration
73from gajim.gui.discovery import ServiceDiscoveryWindow
74from gajim.gui.tooltips import RosterTooltip
75from gajim.gui.adhoc import AdHocCommand
76from gajim.gui.status_selector import StatusSelector
77from gajim.gui.util import get_icon_name
78from gajim.gui.util import resize_window
79from gajim.gui.util import restore_roster_position
80from gajim.gui.util import get_metacontact_surface
81from gajim.gui.util import get_builder
82from gajim.gui.util import set_urgency_hint
83from gajim.gui.util import get_activity_icon_name
84from gajim.gui.util import get_account_activity_icon_name
85from gajim.gui.util import get_account_mood_icon_name
86from gajim.gui.util import get_account_tune_icon_name
87from gajim.gui.util import get_account_location_icon_name
88from gajim.gui.util import open_window
89
90
91log = logging.getLogger('gajim.roster')
92
93@unique
94class Column(IntEnum):
95    IMG = 0  # image to show state (online, new message etc)
96    NAME = 1  # cellrenderer text that holds contact nickname
97    TYPE = 2  # account, group or contact?
98    JID = 3  # the jid of the row
99    ACCOUNT = 4  # cellrenderer text that holds account name
100    MOOD_PIXBUF = 5
101    ACTIVITY_PIXBUF = 6
102    TUNE_ICON = 7
103    LOCATION_ICON = 8
104    AVATAR_IMG = 9  # avatar_sha
105    PADLOCK_PIXBUF = 10  # use for account row only
106    VISIBLE = 11
107
108
109class RosterWindow:
110    """
111    Class for main window of the GTK interface
112    """
113
114    def _get_account_iter(self, name, model=None):
115        """
116        Return the Gtk.TreeIter of the given account or None if not found
117
118        Keyword arguments:
119        name -- the account name
120        model -- the data model (default TreeFilterModel)
121        """
122        if model is None:
123            model = self.modelfilter
124            if model is None:
125                return
126
127        if self.regroup:
128            name = 'MERGED'
129        if name not in self._iters:
130            return None
131        it = self._iters[name]['account']
132
133        if model == self.model or it is None:
134            return it
135        try:
136            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
137            if ok:
138                return it
139            return None
140        except RuntimeError:
141            return None
142
143
144    def _get_group_iter(self, name, account, model=None):
145        """
146        Return the Gtk.TreeIter of the given group or None if not found
147
148        Keyword arguments:
149        name -- the group name
150        account -- the account name
151        model -- the data model (default TreeFilterModel)
152        """
153        if model is None:
154            model = self.modelfilter
155            if model is None:
156                return
157
158        if self.regroup:
159            account = 'MERGED'
160
161        if account not in self._iters:
162            return None
163        if name not in self._iters[account]['groups']:
164            return None
165
166        it = self._iters[account]['groups'][name]
167        if model == self.model or it is None:
168            return it
169        try:
170            (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
171            if ok:
172                return it
173            return None
174        except RuntimeError:
175            return None
176
177
178    def _get_self_contact_iter(self, account, model=None):
179        """
180        Return the Gtk.TreeIter of SelfContact or None if not found
181
182        Keyword arguments:
183        account -- the account of SelfContact
184        model -- the data model (default TreeFilterModel)
185        """
186        jid = app.get_jid_from_account(account)
187        its = self._get_contact_iter(jid, account, model=model)
188        if its:
189            return its[0]
190        return None
191
192
193    def _get_contact_iter(self, jid, account, contact=None, model=None):
194        """
195        Return a list of Gtk.TreeIter of the given contact
196
197        Keyword arguments:
198        jid -- the jid without resource
199        account -- the account
200        contact -- the contact (default None)
201        model -- the data model (default TreeFilterModel)
202        """
203        if model is None:
204            model = self.modelfilter
205            # when closing Gajim model can be none (async pbs?)
206            if model is None:
207                return []
208
209        if not contact:
210            contact = app.contacts.get_first_contact_from_jid(account, jid)
211            if not contact:
212                # We don't know this contact
213                return []
214
215        if account not in self._iters:
216            return []
217
218        if jid not in self._iters[account]['contacts']:
219            return []
220
221        its = self._iters[account]['contacts'][jid]
222
223        if not its:
224            return []
225
226        if model == self.model:
227            return its
228
229        its2 = []
230        for it in its:
231            try:
232                (ok, it) = self.modelfilter.convert_child_iter_to_iter(it)
233                if ok:
234                    its2.append(it)
235            except RuntimeError:
236                pass
237        return its2
238
239    @staticmethod
240    def _iter_is_separator(model, titer):
241        """
242        Return True if the given iter is a separator
243
244        Keyword arguments:
245        model -- the data model
246        iter -- the Gtk.TreeIter to test
247        """
248        if model[titer][0] == 'SEPARATOR':
249            return True
250        return False
251
252#############################################################################
253### Methods for adding and removing roster window items
254#############################################################################
255
256    def add_account(self, account):
257        """
258        Add account to roster and draw it. Do nothing if it is already in
259        """
260        if self._get_account_iter(account):
261            # Will happen on reconnect or for merged accounts
262            return
263
264        if self.regroup:
265            # Merged accounts view
266            show = helpers.get_global_show()
267            it = self.model.append(None, [get_icon_name(show),
268                _('Merged accounts'), 'account', '', 'all', None, None, None,
269                None, None, None, True] + [None] * self.nb_ext_renderers)
270            self._iters['MERGED']['account'] = it
271        else:
272            show = helpers.get_connection_status(account)
273            our_jid = app.get_jid_from_account(account)
274
275            it = self.model.append(None, [get_icon_name(show),
276                GLib.markup_escape_text(account), 'account', our_jid,
277                account, None, None, None, None, None, None, True] +
278                [None] * self.nb_ext_renderers)
279            self._iters[account]['account'] = it
280
281        self.draw_account(account)
282
283
284    def add_account_contacts(self, account, improve_speed=True,
285    draw_contacts=True):
286        """
287        Add all contacts and groups of the given account to roster, draw them
288        and account
289        """
290        if improve_speed:
291            self._before_fill()
292        jids = app.contacts.get_jid_list(account)
293
294        for jid in jids:
295            self.add_contact(jid, account)
296
297        if draw_contacts:
298            # Do not freeze the GUI when drawing the contacts
299            if jids:
300                # Overhead is big, only invoke when needed
301                self._idle_draw_jids_of_account(jids, account)
302
303            # Draw all known groups
304            for group in app.groups[account]:
305                self.draw_group(group, account)
306            self.draw_account(account)
307
308        if improve_speed:
309            self._after_fill()
310
311    def _add_group_iter(self, account, group):
312        """
313        Add a group iter in roster and return the newly created iter
314        """
315        if self.regroup:
316            account_group = 'MERGED'
317        else:
318            account_group = account
319        delimiter = app.connections[account].get_module('Delimiter').delimiter
320        group_splited = group.split(delimiter)
321        parent_group = delimiter.join(group_splited[:-1])
322        if len(group_splited) > 1 and parent_group in self._iters[account_group]['groups']:
323            iter_parent = self._iters[account_group]['groups'][parent_group]
324        elif parent_group:
325            iter_parent = self._add_group_iter(account, parent_group)
326            if parent_group not in app.groups[account]:
327                if account + parent_group in self.collapsed_rows:
328                    is_expanded = False
329                else:
330                    is_expanded = True
331                app.groups[account][parent_group] = {'expand': is_expanded}
332        else:
333            iter_parent = self._get_account_iter(account, self.model)
334        iter_group = self.model.append(iter_parent,
335            [get_icon_name('closed'),
336            GLib.markup_escape_text(group), 'group', group, account, None,
337            None, None, None, None, None, False] + [None] * self.nb_ext_renderers)
338        self.draw_group(group, account)
339        self._iters[account_group]['groups'][group] = iter_group
340        return iter_group
341
342    def _add_entity(self, contact, account, groups=None,
343    big_brother_contact=None, big_brother_account=None):
344        """
345        Add the given contact to roster data model
346
347        Contact is added regardless if he is already in roster or not. Return
348        list of newly added iters.
349
350        Keyword arguments:
351        contact -- the contact to add
352        account -- the contacts account
353        groups -- list of groups to add the contact to.
354                  (default groups in contact.get_shown_groups()).
355                Parameter ignored when big_brother_contact is specified.
356        big_brother_contact -- if specified contact is added as child
357                  big_brother_contact. (default None)
358        """
359        added_iters = []
360        visible = self.contact_is_visible(contact, account)
361        if big_brother_contact:
362            # Add contact under big brother
363
364            parent_iters = self._get_contact_iter(
365                    big_brother_contact.jid, big_brother_account,
366                    big_brother_contact, self.model)
367
368            # Do not confuse get_contact_iter: Sync groups of family members
369            contact.groups = big_brother_contact.groups[:]
370
371            image = self._get_avatar_image(account, contact.jid)
372
373            for child_iter in parent_iters:
374                it = self.model.append(child_iter, [None,
375                    contact.get_shown_name(), 'contact', contact.jid, account,
376                    None, None, None, None, image, None, visible] + \
377                    [None] * self.nb_ext_renderers)
378                added_iters.append(it)
379                if contact.jid in self._iters[account]['contacts']:
380                    self._iters[account]['contacts'][contact.jid].append(it)
381                else:
382                    self._iters[account]['contacts'][contact.jid] = [it]
383        else:
384            # We are a normal contact. Add us to our groups.
385            if not groups:
386                groups = contact.get_shown_groups()
387            for group in groups:
388                child_iterG = self._get_group_iter(group, account,
389                    model=self.model)
390                if not child_iterG:
391                    # Group is not yet in roster, add it!
392                    child_iterG = self._add_group_iter(account, group)
393
394                if contact.is_transport():
395                    typestr = 'agent'
396                elif contact.is_groupchat:
397                    typestr = 'groupchat'
398                else:
399                    typestr = 'contact'
400
401                image = self._get_avatar_image(account, contact.jid)
402
403                # we add some values here. see draw_contact
404                # for more
405                i_ = self.model.append(child_iterG, [None,
406                    contact.get_shown_name(), typestr, contact.jid, account,
407                    None, None, None, None, image, None, visible] + \
408                    [None] * self.nb_ext_renderers)
409                added_iters.append(i_)
410                if contact.jid in self._iters[account]['contacts']:
411                    self._iters[account]['contacts'][contact.jid].append(i_)
412                else:
413                    self._iters[account]['contacts'][contact.jid] = [i_]
414
415                # Restore the group expand state
416                if account + group in self.collapsed_rows:
417                    is_expanded = False
418                else:
419                    is_expanded = True
420                if group not in app.groups[account]:
421                    app.groups[account][group] = {'expand': is_expanded}
422
423        return added_iters
424
425    def _remove_entity(self, contact, account, groups=None):
426        """
427        Remove the given contact from roster data model
428
429        Empty groups after contact removal are removed too.
430        Return False if contact still has children and deletion was
431        not performed.
432        Return True on success.
433
434        Keyword arguments:
435        contact -- the contact to add
436        account -- the contacts account
437        groups -- list of groups to remove the contact from.
438        """
439        iters = self._get_contact_iter(contact.jid, account, contact,
440            self.model)
441
442        parent_iter = self.model.iter_parent(iters[0])
443        parent_type = self.model[parent_iter][Column.TYPE]
444
445        if groups:
446            # Only remove from specified groups
447            all_iters = iters[:]
448            group_iters = [self._get_group_iter(group, account)
449                    for group in groups]
450            iters = [titer for titer in all_iters
451                    if self.model.iter_parent(titer) in group_iters]
452
453        iter_children = self.model.iter_children(iters[0])
454
455        if iter_children:
456            # We have children. We cannot be removed!
457            return False
458        # Remove us and empty groups from the model
459        for i in iters:
460            parent_i = self.model.iter_parent(i)
461            parent_type = self.model[parent_i][Column.TYPE]
462
463            to_be_removed = i
464            while parent_type == 'group' and \
465            self.model.iter_n_children(parent_i) == 1:
466                if self.regroup:
467                    account_group = 'MERGED'
468                else:
469                    account_group = account
470                group = self.model[parent_i][Column.JID]
471                if group in app.groups[account]:
472                    del app.groups[account][group]
473                to_be_removed = parent_i
474                del self._iters[account_group]['groups'][group]
475                parent_i = self.model.iter_parent(parent_i)
476                parent_type = self.model[parent_i][Column.TYPE]
477            self.model.remove(to_be_removed)
478
479        del self._iters[account]['contacts'][contact.jid]
480        return True
481
482    def _add_metacontact_family(self, family, account):
483        """
484        Add the give Metacontact family to roster data model
485
486        Add Big Brother to his groups and all others under him.
487        Return list of all added (contact, account) tuples with
488        Big Brother as first element.
489
490        Keyword arguments:
491        family -- the family, see Contacts.get_metacontacts_family()
492        """
493
494        nearby_family, big_brother_jid, big_brother_account = \
495                self._get_nearby_family_and_big_brother(family, account)
496        if not big_brother_jid:
497            return []
498        big_brother_contact = app.contacts.get_first_contact_from_jid(
499                big_brother_account, big_brother_jid)
500
501        self._add_entity(big_brother_contact, big_brother_account)
502
503        brothers = []
504        # Filter family members
505        for data in nearby_family:
506            _account = data['account']
507            _jid = data['jid']
508            _contact = app.contacts.get_first_contact_from_jid(
509                    _account, _jid)
510
511            if not _contact or _contact == big_brother_contact:
512                # Corresponding account is not connected
513                # or brother already added
514                continue
515
516            self._add_entity(_contact, _account,
517                    big_brother_contact=big_brother_contact,
518                    big_brother_account=big_brother_account)
519            brothers.append((_contact, _account))
520
521        brothers.insert(0, (big_brother_contact, big_brother_account))
522        return brothers
523
524    def _remove_metacontact_family(self, family, account):
525        """
526        Remove the given Metacontact family from roster data model
527
528        See Contacts.get_metacontacts_family() and
529        RosterWindow._remove_entity()
530        """
531        nearby_family = self._get_nearby_family_and_big_brother(
532                family, account)[0]
533
534        # Family might has changed (actual big brother not on top).
535        # Remove children first then big brother
536        family_in_roster = False
537        for data in nearby_family:
538            _account = data['account']
539            _jid = data['jid']
540            _contact = app.contacts.get_first_contact_from_jid(_account, _jid)
541
542            iters = self._get_contact_iter(_jid, _account, _contact, self.model)
543            if not iters or not _contact:
544                # Family might not be up to date.
545                # Only try to remove what is actually in the roster
546                continue
547
548            family_in_roster = True
549
550            parent_iter = self.model.iter_parent(iters[0])
551            parent_type = self.model[parent_iter][Column.TYPE]
552
553            if parent_type != 'contact':
554                # The contact on top
555                old_big_account = _account
556                old_big_contact = _contact
557                continue
558
559            self._remove_entity(_contact, _account)
560
561        if not family_in_roster:
562            return False
563
564        self._remove_entity(old_big_contact, old_big_account)
565
566        return True
567
568    def _recalibrate_metacontact_family(self, family, account):
569        """
570        Regroup metacontact family if necessary
571        """
572
573        brothers = []
574        nearby_family, big_brother_jid, big_brother_account = \
575            self._get_nearby_family_and_big_brother(family, account)
576        big_brother_contact = app.contacts.get_contact(big_brother_account,
577            big_brother_jid)
578        child_iters = self._get_contact_iter(big_brother_jid,
579            big_brother_account, model=self.model)
580        if child_iters:
581            parent_iter = self.model.iter_parent(child_iters[0])
582            parent_type = self.model[parent_iter][Column.TYPE]
583
584            # Check if the current BigBrother has even been before.
585            if parent_type == 'contact':
586                for data in nearby_family:
587                    # recalibrate after remove to keep highlight
588                    if data['jid'] in app.to_be_removed[data['account']]:
589                        return
590
591                self._remove_metacontact_family(family, account)
592                brothers = self._add_metacontact_family(family, account)
593
594                for c, acc in brothers:
595                    self.draw_completely(c.jid, acc)
596
597        # Check is small brothers are under the big brother
598        for child in nearby_family:
599            _jid = child['jid']
600            _account = child['account']
601            if _account == big_brother_account and _jid == big_brother_jid:
602                continue
603            child_iters = self._get_contact_iter(_jid, _account,
604                model=self.model)
605            if not child_iters:
606                continue
607            parent_iter = self.model.iter_parent(child_iters[0])
608            parent_type = self.model[parent_iter][Column.TYPE]
609            if parent_type != 'contact':
610                _contact = app.contacts.get_contact(_account, _jid)
611                self._remove_entity(_contact, _account)
612                self._add_entity(_contact, _account, groups=None,
613                        big_brother_contact=big_brother_contact,
614                        big_brother_account=big_brother_account)
615
616    def _get_nearby_family_and_big_brother(self, family, account):
617        return app.contacts.get_nearby_family_and_big_brother(family, account)
618
619    def _add_self_contact(self, account):
620        """
621        Add account's SelfContact to roster and draw it and the account
622
623        Return the SelfContact contact instance
624        """
625        jid = app.get_jid_from_account(account)
626        contact = app.contacts.get_first_contact_from_jid(account, jid)
627
628        child_iterA = self._get_account_iter(account, self.model)
629        self._iters[account]['contacts'][jid] = [self.model.append(child_iterA,
630            [None, app.nicks[account], 'self_contact', jid, account, None,
631            None, None, None, None, None, True] + [None] * self.nb_ext_renderers)]
632
633        self.draw_completely(jid, account)
634        self.draw_account(account)
635
636        return contact
637
638    def redraw_metacontacts(self, account):
639        for family in app.contacts.iter_metacontacts_families(account):
640            self._recalibrate_metacontact_family(family, account)
641
642    def add_contact(self, jid, account):
643        """
644        Add contact to roster and draw him
645
646        Add contact to all its group and redraw the groups, the contact and the
647        account. If it's a Metacontact, add and draw the whole family.
648        Do nothing if the contact is already in roster.
649
650        Return the added contact instance. If it is a Metacontact return
651        Big Brother.
652
653        Keyword arguments:
654        jid -- the contact's jid or SelfJid to add SelfContact
655        account -- the corresponding account.
656        """
657        contact = app.contacts.get_contact_with_highest_priority(account, jid)
658        if self._get_contact_iter(jid, account, contact, self.model):
659            # If contact already in roster, do nothing
660            return
661
662        if jid == app.get_jid_from_account(account):
663            return self._add_self_contact(account)
664
665        is_observer = contact.is_observer()
666        if is_observer:
667            # if he has a tag, remove it
668            app.contacts.remove_metacontact(account, jid)
669
670        # Add contact to roster
671        family = app.contacts.get_metacontacts_family(account, jid)
672        contacts = []
673        if family:
674            # We have a family. So we are a metacontact.
675            # Add all family members that we shall be grouped with
676            if self.regroup:
677                # remove existing family members to regroup them
678                self._remove_metacontact_family(family, account)
679            contacts = self._add_metacontact_family(family, account)
680        else:
681            # We are a normal contact
682            contacts = [(contact, account), ]
683            self._add_entity(contact, account)
684
685        # Draw the contact and its groups contact
686        if not self.starting:
687            for c, acc in contacts:
688                self.draw_completely(c.jid, acc)
689            for group in contact.get_shown_groups():
690                self.draw_group(group, account)
691                self._adjust_group_expand_collapse_state(group, account)
692            self.draw_account(account)
693
694        return contacts[0][0] # it's contact/big brother with highest priority
695
696    def remove_contact(self, jid, account, force=False, backend=False, maximize=False):
697        """
698        Remove contact from roster
699
700        Remove contact from all its group. Remove empty groups or redraw
701        otherwise.
702        Draw the account.
703        If it's a Metacontact, remove the whole family.
704        Do nothing if the contact is not in roster.
705
706        Keyword arguments:
707        jid -- the contact's jid or SelfJid to remove SelfContact
708        account -- the corresponding account.
709        force -- remove contact even it has pending evens (Default False)
710        backend -- also remove contact instance (Default False)
711        """
712        contact = app.contacts.get_contact_with_highest_priority(account, jid)
713        if not contact:
714            return
715
716        if not force and self.contact_has_pending_roster_events(contact,
717        account):
718            return False
719
720        iters = self._get_contact_iter(jid, account, contact, self.model)
721        if iters:
722            # no more pending events
723            # Remove contact from roster directly
724            family = app.contacts.get_metacontacts_family(account, jid)
725            if family:
726                # We have a family. So we are a metacontact.
727                self._remove_metacontact_family(family, account)
728            else:
729                self._remove_entity(contact, account)
730
731        old_grps = []
732        if backend:
733            if not app.interface.msg_win_mgr.get_control(jid, account) or \
734            force:
735                # If a window is still opened: don't remove contact instance
736                # Remove contact before redrawing, otherwise the old
737                # numbers will still be show
738                if not maximize:
739                    # Don't remove contact when we maximize a room
740                    app.contacts.remove_jid(account, jid, remove_meta=True)
741                if iters:
742                    rest_of_family = [data for data in family
743                        if account != data['account'] or jid != data['jid']]
744                    if rest_of_family:
745                        # reshow the rest of the family
746                        brothers = self._add_metacontact_family(rest_of_family,
747                            account)
748                        for c, acc in brothers:
749                            self.draw_completely(c.jid, acc)
750            else:
751                for c in app.contacts.get_contacts(account, jid):
752                    c.sub = 'none'
753                    c.show = 'not in roster'
754                    c.status = ''
755                    old_grps = c.get_shown_groups()
756                    c.groups = [_('Not in contact list')]
757                    self._add_entity(c, account)
758                    self.draw_contact(jid, account)
759
760        if iters:
761            # Draw all groups of the contact
762            for group in contact.get_shown_groups() + old_grps:
763                self.draw_group(group, account)
764            self.draw_account(account)
765
766        return True
767
768    def rename_self_contact(self, old_jid, new_jid, account):
769        """
770        Rename the self_contact jid
771
772        Keyword arguments:
773        old_jid -- our old jid
774        new_jid -- our new jid
775        account -- the corresponding account.
776        """
777        app.contacts.change_contact_jid(old_jid, new_jid, account)
778        self_iter = self._get_self_contact_iter(account, model=self.model)
779        if not self_iter:
780            return
781        self.model[self_iter][Column.JID] = new_jid
782        self.draw_contact(new_jid, account)
783
784    def minimize_groupchat(self, account, jid, status=''):
785        gc_control = app.interface.msg_win_mgr.get_gc_control(jid, account)
786        app.interface.minimized_controls[account][jid] = gc_control
787        self.add_groupchat(jid, account)
788
789    def add_groupchat(self, jid, account):
790        """
791        Add groupchat to roster and draw it. Return the added contact instance
792        """
793        contact = app.contacts.get_groupchat_contact(account, jid)
794        show = 'offline'
795        if app.account_is_available(account):
796            show = 'online'
797
798        contact.show = show
799        self.add_contact(jid, account)
800
801        return contact
802
803    def remove_groupchat(self, jid, account, maximize=False):
804        """
805        Remove groupchat from roster and redraw account and group
806        """
807        contact = app.contacts.get_contact_with_highest_priority(account, jid)
808        if contact.is_groupchat:
809            if jid in app.interface.minimized_controls[account]:
810                del app.interface.minimized_controls[account][jid]
811            self.remove_contact(jid, account, force=True, backend=True, maximize=maximize)
812            return True
813        return False
814
815    # FIXME: This function is yet unused! Port to new API
816    def add_transport(self, jid, account):
817        """
818        Add transport to roster and draw it. Return the added contact instance
819        """
820        contact = app.contacts.get_contact_with_highest_priority(account, jid)
821        if contact is None:
822            contact = app.contacts.create_contact(jid=jid, account=account,
823                name=jid, groups=[_('Transports')], show='offline',
824                status='offline', sub='from')
825            app.contacts.add_contact(account, contact)
826        self.add_contact(jid, account)
827        return contact
828
829    def remove_transport(self, jid, account):
830        """
831        Remove transport from roster and redraw account and group
832        """
833        self.remove_contact(jid, account, force=True, backend=True)
834        return True
835
836    def rename_group(self, old_name, new_name, account):
837        """
838        Rename a roster group
839        """
840        if old_name == new_name:
841            return
842
843        # Groups may not change name from or to a special groups
844        for g in helpers.special_groups:
845            if g in (new_name, old_name):
846                return
847
848        # update all contacts in the given group
849        if self.regroup:
850            accounts = app.connections.keys()
851        else:
852            accounts = [account, ]
853
854        for acc in accounts:
855            changed_contacts = []
856            for jid in app.contacts.get_jid_list(acc):
857                contact = app.contacts.get_first_contact_from_jid(acc, jid)
858                if old_name not in contact.groups:
859                    continue
860
861                self.remove_contact(jid, acc, force=True)
862
863                contact.groups.remove(old_name)
864                if new_name not in contact.groups:
865                    contact.groups.append(new_name)
866
867                changed_contacts.append({'jid': jid, 'name': contact.name,
868                    'groups':contact.groups})
869
870            app.connections[acc].get_module('Roster').update_contacts(
871                changed_contacts)
872
873            for c in changed_contacts:
874                self.add_contact(c['jid'], acc)
875
876            self._adjust_group_expand_collapse_state(new_name, acc)
877
878            self.draw_group(old_name, acc)
879            self.draw_group(new_name, acc)
880
881
882    def add_contact_to_groups(self, jid, account, groups, update=True):
883        """
884        Add contact to given groups and redraw them
885
886        Contact on server is updated too. When the contact has a family,
887        the action will be performed for all members.
888
889        Keyword Arguments:
890        jid -- the jid
891        account -- the corresponding account
892        groups -- list of Groups to add the contact to.
893        update -- update contact on the server
894        """
895        self.remove_contact(jid, account, force=True)
896        for contact in app.contacts.get_contacts(account, jid):
897            for group in groups:
898                if group not in contact.groups:
899                    # we might be dropped from meta to group
900                    contact.groups.append(group)
901            if update:
902                con = app.connections[account]
903                con.get_module('Roster').update_contact(
904                    jid, contact.name, contact.groups)
905
906        self.add_contact(jid, account)
907
908        for group in groups:
909            self._adjust_group_expand_collapse_state(group, account)
910
911    def remove_contact_from_groups(self, jid, account, groups, update=True):
912        """
913        Remove contact from given groups and redraw them
914
915        Contact on server is updated too. When the contact has a family,
916        the action will be performed for all members.
917
918        Keyword Arguments:
919        jid -- the jid
920        account -- the corresponding account
921        groups -- list of Groups to remove the contact from
922        update -- update contact on the server
923        """
924        self.remove_contact(jid, account, force=True)
925        for contact in app.contacts.get_contacts(account, jid):
926            for group in groups:
927                if group in contact.groups:
928                    # Needed when we remove from "General" or "Observers"
929                    contact.groups.remove(group)
930            if update:
931                con = app.connections[account]
932                con.get_module('Roster').update_contact(
933                    jid, contact.name, contact.groups)
934        self.add_contact(jid, account)
935
936        # Also redraw old groups
937        for group in groups:
938            self.draw_group(group, account)
939
940    # FIXME: maybe move to app.py
941    def remove_newly_added(self, jid, account):
942        if account not in app.newly_added:
943            # Account has been deleted during the timeout that called us
944            return
945        if jid in app.newly_added[account]:
946            app.newly_added[account].remove(jid)
947            self.draw_contact(jid, account)
948
949    # FIXME: maybe move to app.py
950    def remove_to_be_removed(self, jid, account):
951        if account not in app.interface.instances:
952            # Account has been deleted during the timeout that called us
953            return
954        if jid in app.newly_added[account]:
955            return
956        if jid in app.to_be_removed[account]:
957            app.to_be_removed[account].remove(jid)
958            family = app.contacts.get_metacontacts_family(account, jid)
959            if family:
960                # Perform delayed recalibration
961                self._recalibrate_metacontact_family(family, account)
962            self.draw_contact(jid, account)
963            # Hide Group if all children are hidden
964            contact = app.contacts.get_contact(account, jid)
965            if not contact:
966                return
967            for group in contact.get_shown_groups():
968                self.draw_group(group, account)
969
970    # FIXME: integrate into add_contact()
971    def add_to_not_in_the_roster(self, account, jid, nick='', resource='',
972                                 groupchat=False):
973        contact = app.contacts.create_not_in_roster_contact(
974            jid=jid, account=account, resource=resource, name=nick,
975            groupchat=groupchat)
976        app.contacts.add_contact(account, contact)
977        self.add_contact(contact.jid, account)
978        return contact
979
980
981################################################################################
982### Methods for adding and removing roster window items
983################################################################################
984
985    def _really_draw_account(self, account):
986        child_iter = self._get_account_iter(account, self.model)
987        if not child_iter:
988            return
989
990        if self.regroup:
991            account_name = _('Merged accounts')
992            accounts = []
993        else:
994            account_name = app.get_account_label(account)
995            accounts = [account]
996
997        if account in self.collapsed_rows and \
998        self.model.iter_has_child(child_iter):
999            account_name = '[%s]' % account_name
1000
1001        if (app.account_is_available(account) or (self.regroup and \
1002        app.get_number_of_connected_accounts())) and app.settings.get(
1003        'show_contacts_number'):
1004            nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts(
1005                    accounts=accounts)
1006            account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
1007
1008        self.model[child_iter][Column.NAME] = GLib.markup_escape_text(account_name)
1009
1010        mood_icon_name = get_account_mood_icon_name(account)
1011        self.model[child_iter][Column.MOOD_PIXBUF] = mood_icon_name
1012
1013        activity_icon_name = get_account_activity_icon_name(account)
1014        self.model[child_iter][Column.ACTIVITY_PIXBUF] = activity_icon_name
1015
1016        tune_icon_name = get_account_tune_icon_name(account)
1017        self.model[child_iter][Column.TUNE_ICON] = tune_icon_name
1018
1019        location_icon_name = get_account_location_icon_name(account)
1020        self.model[child_iter][Column.LOCATION_ICON] = location_icon_name
1021
1022    def _really_draw_accounts(self):
1023        for acct in self.accounts_to_draw:
1024            self._really_draw_account(acct)
1025        self.accounts_to_draw = []
1026        return False
1027
1028    def draw_account(self, account):
1029        if account in self.accounts_to_draw:
1030            return
1031        self.accounts_to_draw.append(account)
1032        if len(self.accounts_to_draw) == 1:
1033            GLib.timeout_add(200, self._really_draw_accounts)
1034
1035    def _really_draw_group(self, group, account):
1036        child_iter = self._get_group_iter(group, account, model=self.model)
1037        if not child_iter:
1038            # Eg. We redraw groups after we removed a entity
1039            # and its empty groups
1040            return
1041        if self.regroup:
1042            accounts = []
1043        else:
1044            accounts = [account]
1045        text = GLib.markup_escape_text(group)
1046        if app.settings.get('show_contacts_number'):
1047            nbr_on, nbr_total = app.contacts.get_nb_online_total_contacts(
1048                    accounts=accounts, groups=[group])
1049            text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total))
1050
1051        self.model[child_iter][Column.NAME] = text
1052
1053        # Hide group if no more contacts
1054        iterG = self._get_group_iter(group, account, model=self.modelfilter)
1055        to_hide = []
1056        while iterG:
1057            parent = self.modelfilter.iter_parent(iterG)
1058            if (not self.modelfilter.iter_has_child(iterG)) or (to_hide \
1059            and self.modelfilter.iter_n_children(iterG) == 1):
1060                to_hide.append(iterG)
1061                if not parent or self.modelfilter[parent][Column.TYPE] != \
1062                'group':
1063                    iterG = None
1064                else:
1065                    iterG = parent
1066            else:
1067                iterG = None
1068        for iter_ in to_hide:
1069            self.modelfilter[iter_][Column.VISIBLE] = False
1070
1071    def _really_draw_groups(self):
1072        for ag in self.groups_to_draw.values():
1073            acct = ag['account']
1074            grp = ag['group']
1075            self._really_draw_group(grp, acct)
1076        self.groups_to_draw = {}
1077        return False
1078
1079    def draw_group(self, group, account):
1080        ag = account + group
1081        if ag in self.groups_to_draw:
1082            return
1083        self.groups_to_draw[ag] = {'group': group, 'account': account}
1084        if len(self.groups_to_draw) == 1:
1085            GLib.timeout_add(200, self._really_draw_groups)
1086
1087    def draw_parent_contact(self, jid, account):
1088        child_iters = self._get_contact_iter(jid, account, model=self.model)
1089        if not child_iters:
1090            return False
1091        parent_iter = self.model.iter_parent(child_iters[0])
1092        if self.model[parent_iter][Column.TYPE] != 'contact':
1093            # parent is not a contact
1094            return
1095        parent_jid = self.model[parent_iter][Column.JID]
1096        parent_account = self.model[parent_iter][Column.ACCOUNT]
1097        self.draw_contact(parent_jid, parent_account)
1098        return False
1099
1100    def draw_contact(self, jid, account, selected=False, focus=False,
1101    contact_instances=None, contact=None):
1102        """
1103        Draw the correct state image, name BUT not avatar
1104        """
1105        # focus is about if the roster window has toplevel-focus or not
1106        # FIXME: We really need a custom cell_renderer
1107
1108        if not contact_instances:
1109            contact_instances = app.contacts.get_contacts(account, jid)
1110        if not contact:
1111            contact = app.contacts.get_highest_prio_contact_from_contacts(
1112                contact_instances)
1113        if not contact:
1114            return False
1115
1116        child_iters = self._get_contact_iter(jid, account, contact, self.model)
1117        if not child_iters:
1118            return False
1119
1120        name = GLib.markup_escape_text(contact.get_shown_name())
1121
1122        # gets number of unread gc marked messages
1123        if jid in app.interface.minimized_controls[account] and \
1124        app.interface.minimized_controls[account][jid]:
1125            nb_unread = len(app.events.get_events(account, jid,
1126                    ['printed_marked_gc_msg']))
1127            nb_unread += app.interface.minimized_controls \
1128                    [account][jid].get_nb_unread_pm()
1129
1130            if nb_unread == 1:
1131                name = '%s *' % name
1132            elif nb_unread > 1:
1133                name = '%s [%s]' % (name, str(nb_unread))
1134
1135        # Strike name if blocked
1136        strike = helpers.jid_is_blocked(account, jid)
1137        if strike:
1138            name = '<span strikethrough="true">%s</span>' % name
1139
1140        # Show resource counter
1141        nb_connected_contact = 0
1142        for c in contact_instances:
1143            if c.show not in ('error', 'offline'):
1144                nb_connected_contact += 1
1145        if nb_connected_contact > 1:
1146            # switch back to default writing direction
1147            name += i18n.paragraph_direction_mark(name)
1148            name += ' (%d)' % nb_connected_contact
1149
1150        # add status msg, if not empty, under contact name in
1151        # the treeview
1152        if app.settings.get('show_status_msgs_in_roster'):
1153            status_span = '\n<span size="small" style="italic" ' \
1154                          'alpha="70%">{}</span>'
1155            if contact.is_groupchat:
1156                disco_info = app.storage.cache.get_last_disco_info(contact.jid)
1157                if disco_info is not None:
1158                    description = disco_info.muc_description
1159                    if description:
1160                        name += status_span.format(
1161                            GLib.markup_escape_text(description))
1162            elif contact.status:
1163                status = contact.status.strip()
1164                if status != '':
1165                    status = helpers.reduce_chars_newlines(
1166                        status, max_lines=1)
1167                    name += status_span.format(
1168                        GLib.markup_escape_text(status))
1169
1170        icon_name = helpers.get_icon_name_to_show(contact, account)
1171        # look if another resource has awaiting events
1172        for c in contact_instances:
1173            c_icon_name = helpers.get_icon_name_to_show(c, account)
1174            if c_icon_name in ('event', 'muc-active', 'muc-inactive'):
1175                icon_name = c_icon_name
1176                break
1177
1178        # Check for events of collapsed (hidden) brothers
1179        family = app.contacts.get_metacontacts_family(account, jid)
1180        is_big_brother = False
1181        have_visible_children = False
1182        if family:
1183            bb_jid, bb_account = \
1184                self._get_nearby_family_and_big_brother(family, account)[1:]
1185            is_big_brother = (jid, account) == (bb_jid, bb_account)
1186            iters = self._get_contact_iter(jid, account)
1187            have_visible_children = iters and \
1188                self.modelfilter.iter_has_child(iters[0])
1189
1190        if have_visible_children:
1191            # We are the big brother and have a visible family
1192            for child_iter in child_iters:
1193                child_path = self.model.get_path(child_iter)
1194                path = self.modelfilter.convert_child_path_to_path(child_path)
1195
1196                if not path:
1197                    continue
1198
1199                if not self.tree.row_expanded(path) and icon_name != 'event':
1200                    iterC = self.model.iter_children(child_iter)
1201                    while iterC:
1202                        # a child has awaiting messages?
1203                        jidC = self.model[iterC][Column.JID]
1204                        accountC = self.model[iterC][Column.ACCOUNT]
1205                        if app.events.get_events(accountC, jidC):
1206                            icon_name = 'event'
1207                            break
1208                        iterC = self.model.iter_next(iterC)
1209
1210                if self.tree.row_expanded(path):
1211                    icon_name += ':opened'
1212                else:
1213                    icon_name += ':closed'
1214
1215                theme_icon = get_icon_name(icon_name)
1216                self.model[child_iter][Column.IMG] = theme_icon
1217                self.model[child_iter][Column.NAME] = name
1218                #TODO: compute visible
1219                visible = True
1220                self.model[child_iter][Column.VISIBLE] = visible
1221        else:
1222            # A normal contact or little brother
1223            transport = app.get_transport_name_from_jid(jid)
1224            if transport == 'jabber':
1225                transport = None
1226            theme_icon = get_icon_name(icon_name, transport=transport)
1227
1228            visible = self.contact_is_visible(contact, account)
1229            # All iters have the same icon (no expand/collapse)
1230            for child_iter in child_iters:
1231                self.model[child_iter][Column.IMG] = theme_icon
1232                self.model[child_iter][Column.NAME] = name
1233                self.model[child_iter][Column.VISIBLE] = visible
1234                if visible:
1235                    parent_iter = self.model.iter_parent(child_iter)
1236                    self.model[parent_iter][Column.VISIBLE] = True
1237
1238            # We are a little brother
1239            if family and not is_big_brother and not self.starting:
1240                self.draw_parent_contact(jid, account)
1241
1242        if visible:
1243            delimiter = app.connections[account].get_module('Delimiter').delimiter
1244            for group in contact.get_shown_groups():
1245                group_splited = group.split(delimiter)
1246                i = 1
1247                while i < len(group_splited) + 1:
1248                    g = delimiter.join(group_splited[:i])
1249                    iterG = self._get_group_iter(g, account, model=self.model)
1250                    if iterG:
1251                        # it's not self contact
1252                        self.model[iterG][Column.VISIBLE] = True
1253                    i += 1
1254
1255        app.plugin_manager.gui_extension_point('roster_draw_contact', self,
1256            jid, account, contact)
1257
1258        return False
1259
1260    def _is_pep_shown_in_roster(self, pep_type):
1261        if pep_type == PEPEventType.MOOD:
1262            return app.settings.get('show_mood_in_roster')
1263
1264        if pep_type == PEPEventType.ACTIVITY:
1265            return app.settings.get('show_activity_in_roster')
1266
1267        if pep_type == PEPEventType.TUNE:
1268            return  app.settings.get('show_tunes_in_roster')
1269
1270        if pep_type == PEPEventType.LOCATION:
1271            return  app.settings.get('show_location_in_roster')
1272
1273        return False
1274
1275    def draw_all_pep_types(self, jid, account, contact=None):
1276        self._draw_pep(account, jid, PEPEventType.MOOD)
1277        self._draw_pep(account, jid, PEPEventType.ACTIVITY)
1278        self._draw_pep(account, jid, PEPEventType.TUNE)
1279        self._draw_pep(account, jid, PEPEventType.LOCATION)
1280
1281    def _draw_pep(self, account, jid, type_):
1282        if not self._is_pep_shown_in_roster(type_):
1283            return
1284
1285        iters = self._get_contact_iter(jid, account, model=self.model)
1286        if not iters:
1287            return
1288        contact = app.contacts.get_contact(account, jid)
1289
1290        icon = None
1291        data = contact.pep.get(type_)
1292
1293        if type_ == PEPEventType.MOOD:
1294            column = Column.MOOD_PIXBUF
1295            if data is not None:
1296                icon = 'mood-%s' % data.mood
1297        elif type_ == PEPEventType.ACTIVITY:
1298            column = Column.ACTIVITY_PIXBUF
1299            if data is not None:
1300                icon = get_activity_icon_name(data.activity, data.subactivity)
1301        elif type_ == PEPEventType.TUNE:
1302            column = Column.TUNE_ICON
1303            if data is not None:
1304                icon = 'audio-x-generic'
1305        elif type_ == PEPEventType.LOCATION:
1306            column = Column.LOCATION_ICON
1307            if data is not None:
1308                icon = 'applications-internet'
1309
1310        for child_iter in iters:
1311            self.model[child_iter][column] = icon
1312
1313    def _get_avatar_image(self, account, jid):
1314        if not app.settings.get('show_avatars_in_roster'):
1315            return None
1316        scale = self.window.get_scale_factor()
1317        surface = app.contacts.get_avatar(
1318            account, jid, AvatarSize.ROSTER, scale)
1319        return Gtk.Image.new_from_surface(surface)
1320
1321    def draw_avatar(self, jid, account):
1322        iters = self._get_contact_iter(jid, account, model=self.model)
1323        if not iters or not app.settings.get('show_avatars_in_roster'):
1324            return
1325        jid = self.model[iters[0]][Column.JID]
1326        image = self._get_avatar_image(account, jid)
1327
1328        for child_iter in iters:
1329            self.model[child_iter][Column.AVATAR_IMG] = image
1330        return False
1331
1332    def draw_completely(self, jid, account):
1333        contact_instances = app.contacts.get_contacts(account, jid)
1334        contact = app.contacts.get_highest_prio_contact_from_contacts(
1335            contact_instances)
1336        self.draw_contact(
1337            jid, account,
1338            contact_instances=contact_instances,
1339            contact=contact)
1340
1341    def adjust_and_draw_contact_context(self, jid, account):
1342        """
1343        Draw contact, account and groups of given jid Show contact if it has
1344        pending events
1345        """
1346        contact = app.contacts.get_first_contact_from_jid(account, jid)
1347        if not contact:
1348            # idle draw or just removed SelfContact
1349            return
1350
1351        family = app.contacts.get_metacontacts_family(account, jid)
1352        if family:
1353            # There might be a new big brother
1354            self._recalibrate_metacontact_family(family, account)
1355        self.draw_contact(jid, account)
1356        self.draw_account(account)
1357
1358        for group in contact.get_shown_groups():
1359            self.draw_group(group, account)
1360            self._adjust_group_expand_collapse_state(group, account)
1361
1362    def _idle_draw_jids_of_account(self, jids, account):
1363        """
1364        Draw given contacts and their avatars in a lazy fashion
1365
1366        Keyword arguments:
1367        jids -- a list of jids to draw
1368        account -- the corresponding account
1369        """
1370        def _draw_all_contacts(jids, account):
1371            for jid in jids:
1372                family = app.contacts.get_metacontacts_family(account, jid)
1373                if family:
1374                    # For metacontacts over several accounts:
1375                    # When we connect a new account existing brothers
1376                    # must be redrawn (got removed and added again)
1377                    for data in family:
1378                        self.draw_completely(data['jid'], data['account'])
1379                else:
1380                    self.draw_completely(jid, account)
1381                yield True
1382            self.refilter_shown_roster_items()
1383            yield False
1384
1385        task = _draw_all_contacts(jids, account)
1386        GLib.idle_add(next, task)
1387
1388    def _before_fill(self):
1389        self.tree.freeze_child_notify()
1390        self.tree.set_model(None)
1391        # disable sorting
1392        self.model.set_sort_column_id(-2, Gtk.SortType.ASCENDING)
1393        self.starting = True
1394        self.starting_filtering = True
1395
1396    def _after_fill(self):
1397        self.starting = False
1398        accounts_list = app.contacts.get_accounts()
1399        for account in app.connections:
1400            if account not in accounts_list:
1401                continue
1402
1403            jids = app.contacts.get_jid_list(account)
1404            for jid in jids:
1405                self.draw_completely(jid, account)
1406
1407            # Draw all known groups
1408            for group in app.groups[account]:
1409                self.draw_group(group, account)
1410            self.draw_account(account)
1411
1412        self.model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
1413        self.tree.set_model(self.modelfilter)
1414        self.tree.thaw_child_notify()
1415        self.starting_filtering = False
1416        self.refilter_shown_roster_items()
1417
1418    def setup_and_draw_roster(self):
1419        """
1420        Create new empty model and draw roster
1421        """
1422        self.modelfilter = None
1423        self.model = Gtk.TreeStore(*self.columns)
1424
1425        self.model.set_sort_func(1, self._compareIters)
1426        self.model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
1427        self.modelfilter = self.model.filter_new()
1428        self.modelfilter.set_visible_func(self._visible_func)
1429        self.modelfilter.connect('row-has-child-toggled',
1430                self.on_modelfilter_row_has_child_toggled)
1431        self.tree.set_model(self.modelfilter)
1432
1433        self._iters = {}
1434        # for merged mode
1435        self._iters['MERGED'] = {'account': None, 'groups': {}}
1436        for acct in app.contacts.get_accounts():
1437            self._iters[acct] = {'account': None, 'groups': {}, 'contacts': {}}
1438
1439        for acct in app.contacts.get_accounts():
1440            self.add_account(acct)
1441            self.add_account_contacts(acct, improve_speed=True,
1442                draw_contacts=False)
1443
1444        # Recalculate column width for ellipsizing
1445        self.tree.columns_autosize()
1446
1447    def update_status_selector(self):
1448        self._status_selector.update()
1449
1450    def select_contact(self, jid, account):
1451        """
1452        Select contact in roster. If contact is hidden but has events, show him
1453        """
1454        # Refiltering SHOULD NOT be needed:
1455        # When a contact gets a new event he will be redrawn and his
1456        # icon changes, so _visible_func WILL be called on him anyway
1457        iters = self._get_contact_iter(jid, account)
1458        if not iters:
1459            # Not visible in roster
1460            return
1461        path = self.modelfilter.get_path(iters[0])
1462        if self.dragging or not app.settings.get(
1463        'scroll_roster_to_last_message'):
1464            # do not change selection while DND'ing
1465            return
1466        # Expand his parent, so this path is visible, don't expand it.
1467        path.up()
1468        self.tree.expand_to_path(path)
1469        self.tree.scroll_to_cell(path)
1470        self.tree.set_cursor(path)
1471
1472    def _readjust_expand_collapse_state(self):
1473        def func(model, path, iter_, param):
1474            type_ = model[iter_][Column.TYPE]
1475            acct = model[iter_][Column.ACCOUNT]
1476            jid = model[iter_][Column.JID]
1477            key = None
1478            if type_ == 'account':
1479                key = acct
1480            elif type_ == 'group':
1481                key = acct + jid
1482            elif type_ == 'contact':
1483                parent_iter = model.iter_parent(iter_)
1484                ptype = model[parent_iter][Column.TYPE]
1485                if ptype == 'group':
1486                    grp = model[parent_iter][Column.JID]
1487                    key = acct + grp + jid
1488            if key:
1489                if key in self.collapsed_rows:
1490                    self.tree.collapse_row(path)
1491                else:
1492                    self.tree.expand_row(path, False)
1493        self.modelfilter.foreach(func, None)
1494
1495    def _adjust_account_expand_collapse_state(self, account):
1496        """
1497        Expand/collapse account row based on self.collapsed_rows
1498        """
1499        if not self.tree.get_model():
1500            return
1501        iterA = self._get_account_iter(account)
1502        if not iterA:
1503            # thank you modelfilter
1504            return
1505        path = self.modelfilter.get_path(iterA)
1506        if account in self.collapsed_rows:
1507            self.tree.collapse_row(path)
1508        else:
1509            self.tree.expand_row(path, False)
1510        return False
1511
1512
1513    def _adjust_group_expand_collapse_state(self, group, account):
1514        """
1515        Expand/collapse group row based on self.collapsed_rows
1516        """
1517        if not self.tree.get_model():
1518            return
1519        if account not in app.connections:
1520            return
1521        delimiter = app.connections[account].get_module('Delimiter').delimiter
1522        group_splited = group.split(delimiter)
1523        i = 1
1524        while i < len(group_splited) + 1:
1525            g = delimiter.join(group_splited[:i])
1526            iterG = self._get_group_iter(g, account)
1527            if not iterG:
1528                # Group not visible
1529                return
1530            path = self.modelfilter.get_path(iterG)
1531            if account + g in self.collapsed_rows:
1532                self.tree.collapse_row(path)
1533            else:
1534                self.tree.expand_row(path, False)
1535            i += 1
1536
1537##############################################################################
1538### Roster and Modelfilter handling
1539##############################################################################
1540
1541    def refilter_shown_roster_items(self):
1542        if self.filtering:
1543            return
1544        self.filtering = True
1545        for account in app.connections:
1546            for jid in app.contacts.get_jid_list(account):
1547                self.adjust_and_draw_contact_context(jid, account)
1548        self.filtering = False
1549
1550    def contact_has_pending_roster_events(self, contact, account):
1551        """
1552        Return True if the contact or one if it resources has pending events
1553        """
1554        # jid has pending events
1555        if app.events.get_nb_roster_events(account, contact.jid) > 0:
1556            return True
1557        # check events of all resources
1558        for contact_ in app.contacts.get_contacts(account, contact.jid):
1559            if contact_.resource and app.events.get_nb_roster_events(account,
1560            contact_.get_full_jid()) > 0:
1561                return True
1562        return False
1563
1564    def contact_is_visible(self, contact, account):
1565        if self.rfilter_enabled:
1566            return self.rfilter_string in contact.get_shown_name().lower()
1567        if self.contact_has_pending_roster_events(contact, account):
1568            return True
1569        if app.settings.get('showoffline'):
1570            return True
1571
1572        if contact.show in ('offline', 'error'):
1573            if contact.jid in app.to_be_removed[account]:
1574                return True
1575            return False
1576        if app.settings.get('show_only_chat_and_online') and contact.show in (
1577        'away', 'xa', 'busy'):
1578            return False
1579        if _('Transports') in contact.get_shown_groups():
1580            return app.settings.get('show_transports_group')
1581        return True
1582
1583    def _visible_func(self, model, titer, dummy):
1584        """
1585        Determine whether iter should be visible in the treeview
1586        """
1587        if self.starting_filtering:
1588            return False
1589
1590        visible = model[titer][Column.VISIBLE]
1591
1592        type_ = model[titer][Column.TYPE]
1593        if not type_:
1594            return False
1595        if type_ == 'account':
1596            # Always show account
1597            return True
1598
1599        account = model[titer][Column.ACCOUNT]
1600        if not account:
1601            return False
1602
1603        jid = model[titer][Column.JID]
1604        if not jid:
1605            return False
1606
1607        if not self.rfilter_enabled:
1608            return visible
1609
1610        if type_ == 'group':
1611            group = jid
1612            if group == _('Transports'):
1613                if self.regroup:
1614                    accounts = app.contacts.get_accounts()
1615                else:
1616                    accounts = [account]
1617                for _acc in accounts:
1618                    for contact in app.contacts.iter_contacts(_acc):
1619                        if group in contact.get_shown_groups():
1620                            if self.rfilter_string in \
1621                            contact.get_shown_name().lower():
1622                                return True
1623                        elif self.contact_has_pending_roster_events(contact,
1624                        _acc):
1625                            return True
1626                    # No transport has been found
1627                    return False
1628
1629        if type_ == 'contact':
1630            if model.iter_has_child(titer):
1631                iter_c = model.iter_children(titer)
1632                while iter_c:
1633                    if self.rfilter_string in model[iter_c][Column.NAME].lower():
1634                        return True
1635                    iter_c = model.iter_next(iter_c)
1636            return self.rfilter_string in model[titer][Column.NAME].lower()
1637
1638        if type_ == 'agent':
1639            return self.rfilter_string in model[titer][Column.NAME].lower()
1640
1641        if type_ == 'groupchat':
1642            return self.rfilter_string in model[titer][Column.NAME].lower()
1643
1644        return visible
1645
1646    def _compareIters(self, model, iter1, iter2, data=None):
1647        """
1648        Compare two iters to sort them
1649        """
1650        name1 = model[iter1][Column.NAME]
1651        name2 = model[iter2][Column.NAME]
1652        if not name1 or not name2:
1653            return 0
1654        type1 = model[iter1][Column.TYPE]
1655        type2 = model[iter2][Column.TYPE]
1656        if type1 == 'self_contact':
1657            return -1
1658        if type2 == 'self_contact':
1659            return 1
1660        if type1 == 'group':
1661            name1 = model[iter1][Column.JID]
1662            name2 = model[iter2][Column.JID]
1663            if name1 == _('Transports'):
1664                return 1
1665            if name2 == _('Transports'):
1666                return -1
1667            if name1 == _('Not in contact list'):
1668                return 1
1669            if name2 == _('Not in contact list'):
1670                return -1
1671            if name1 == _('Group chats'):
1672                return 1
1673            if name2 == _('Group chats'):
1674                return -1
1675        account1 = model[iter1][Column.ACCOUNT]
1676        account2 = model[iter2][Column.ACCOUNT]
1677        if not account1 or not account2:
1678            return 0
1679        if type1 == 'account':
1680            return locale.strcoll(account1, account2)
1681        jid1 = model[iter1][Column.JID]
1682        jid2 = model[iter2][Column.JID]
1683        if type1 == 'contact':
1684            lcontact1 = app.contacts.get_contacts(account1, jid1)
1685            contact1 = app.contacts.get_first_contact_from_jid(account1, jid1)
1686            if not contact1:
1687                return 0
1688            name1 = contact1.get_shown_name()
1689        if type2 == 'contact':
1690            lcontact2 = app.contacts.get_contacts(account2, jid2)
1691            contact2 = app.contacts.get_first_contact_from_jid(account2, jid2)
1692            if not contact2:
1693                return 0
1694            name2 = contact2.get_shown_name()
1695        # We first compare by show if sort_by_show_in_roster is True or if it's
1696        # a child contact
1697        if type1 == 'contact' and type2 == 'contact' and \
1698        app.settings.get('sort_by_show_in_roster'):
1699            cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4,
1700                     'offline': 6, 'not in roster': 7, 'error': 8}
1701            s = self.get_show(lcontact1)
1702            show1 = cshow.get(s, 9)
1703            s = self.get_show(lcontact2)
1704            show2 = cshow.get(s, 9)
1705            removing1 = False
1706            removing2 = False
1707            if show1 == 6 and jid1 in app.to_be_removed[account1]:
1708                removing1 = True
1709            if show2 == 6 and jid2 in app.to_be_removed[account2]:
1710                removing2 = True
1711            if removing1 and not removing2:
1712                return 1
1713            if removing2 and not removing1:
1714                return -1
1715            sub1 = contact1.sub
1716            sub2 = contact2.sub
1717            # none and from goes after
1718            if sub1 not in ['none', 'from'] and sub2 in ['none', 'from']:
1719                return -1
1720            if sub1 in ['none', 'from'] and sub2 not in ['none', 'from']:
1721                return 1
1722            if show1 < show2:
1723                return -1
1724            if show1 > show2:
1725                return 1
1726        # We compare names
1727        cmp_result = locale.strcoll(name1.lower(), name2.lower())
1728        if cmp_result < 0:
1729            return -1
1730        if cmp_result > 0:
1731            return 1
1732        if type1 == 'contact' and type2 == 'contact':
1733            # We compare account names
1734            cmp_result = locale.strcoll(account1.lower(), account2.lower())
1735            if cmp_result < 0:
1736                return -1
1737            if cmp_result > 0:
1738                return 1
1739            # We compare jids
1740            cmp_result = locale.strcoll(jid1.lower(), jid2.lower())
1741            if cmp_result < 0:
1742                return -1
1743            if cmp_result > 0:
1744                return 1
1745        return 0
1746
1747################################################################################
1748### FIXME: Methods that don't belong to roster window...
1749###             ... at least not in there current form
1750################################################################################
1751
1752    def fire_up_unread_messages_events(self, account):
1753        """
1754        Read from db the unread messages, and fire them up, and if we find very
1755        old unread messages, delete them from unread table
1756        """
1757        results = app.storage.archive.get_unread_msgs()
1758        for result, shown in results:
1759            jid = result.jid
1760            additional_data = result.additional_data
1761            if app.contacts.get_first_contact_from_jid(account, jid) and not \
1762            shown:
1763                # We have this jid in our contacts list
1764                # XXX unread messages should probably have their session saved
1765                # with them
1766                session = app.connections[account].make_new_session(jid)
1767
1768                tim = float(result.time)
1769                session.roster_message(jid, result.message, tim, msg_type='chat',
1770                    msg_log_id=result.log_line_id, additional_data=additional_data)
1771                app.storage.archive.set_shown_unread_msgs(result.log_line_id)
1772
1773            elif (time.time() - result.time) > 2592000:
1774                # ok, here we see that we have a message in unread messages
1775                # table that is older than a month. It is probably from someone
1776                # not in our roster for accounts we usually launch, so we will
1777                # delete this id from unread message tables.
1778                app.storage.archive.set_read_messages([result.log_line_id])
1779
1780    def fill_contacts_and_groups_dicts(self, array, account):
1781        """
1782        Fill app.contacts and app.groups
1783        """
1784        # FIXME: This function needs to be split
1785        # Most of the logic SHOULD NOT be done at GUI level
1786        if account not in app.contacts.get_accounts():
1787            app.contacts.add_account(account)
1788        if not account in self._iters:
1789            self._iters[account] = {'account': None, 'groups': {},
1790                'contacts': {}}
1791        if account not in app.groups:
1792            app.groups[account] = {}
1793
1794        self_jid = str(app.connections[account].get_own_jid())
1795        if account != app.ZEROCONF_ACC_NAME:
1796            array[self_jid] = {'name': app.nicks[account],
1797                               'groups': ['self_contact'],
1798                               'subscription': 'both',
1799                               'ask': 'none'}
1800
1801        # .keys() is needed
1802        for jid in list(array.keys()):
1803            # Remove the contact in roster. It might has changed
1804            self.remove_contact(jid, account, force=True)
1805            # Remove old Contact instances
1806            app.contacts.remove_jid(account, jid, remove_meta=False)
1807            jids = jid.split('/')
1808            # get jid
1809            ji = jids[0]
1810            # get resource
1811            resource = ''
1812            if len(jids) > 1:
1813                resource = '/'.join(jids[1:])
1814            # get name
1815            name = array[jid]['name'] or ''
1816            show = 'offline' # show is offline by default
1817            status = '' # no status message by default
1818
1819            if app.jid_is_transport(jid):
1820                array[jid]['groups'] = [_('Transports')]
1821            #TRANSP - potential
1822            contact1 = app.contacts.create_contact(jid=ji, account=account,
1823                name=name, groups=array[jid]['groups'], show=show,
1824                status=status, sub=array[jid]['subscription'],
1825                ask=array[jid]['ask'], resource=resource)
1826            app.contacts.add_contact(account, contact1)
1827
1828            # If we already have chat windows opened, update them with new
1829            # contact instance
1830            chat_control = app.interface.msg_win_mgr.get_control(ji, account)
1831            if chat_control:
1832                chat_control.contact = contact1
1833
1834    def connected_rooms(self, account):
1835        if account in list(app.gc_connected[account].values()):
1836            return True
1837        return False
1838
1839    def on_event_removed(self, event_list):
1840        """
1841        Remove contacts on last events removed
1842
1843        Only performed if removal was requested before but the contact still had
1844        pending events
1845        """
1846
1847        msg_log_ids = []
1848        for ev in event_list:
1849            if ev.type_ != 'printed_chat':
1850                continue
1851            if ev.msg_log_id:
1852                # There is a msg_log_id
1853                msg_log_ids.append(ev.msg_log_id)
1854
1855        if msg_log_ids:
1856            app.storage.archive.set_read_messages(msg_log_ids)
1857
1858        contact_list = ((event.jid.split('/')[0], event.account) for event in \
1859                event_list)
1860
1861        for jid, account in contact_list:
1862            self.draw_contact(jid, account)
1863            # Remove contacts in roster if removal was requested
1864            key = (jid, account)
1865            if key in list(self.contacts_to_be_removed.keys()):
1866                backend = self.contacts_to_be_removed[key]['backend']
1867                del self.contacts_to_be_removed[key]
1868                # Remove contact will delay removal if there are more events
1869                # pending
1870                self.remove_contact(jid, account, backend=backend)
1871        self.show_title()
1872
1873    def open_event(self, account, jid, event):
1874        """
1875        If an event was handled, return True, else return False
1876        """
1877        ft = app.interface.instances['file_transfers']
1878        event = app.events.get_first_event(account, jid, event.type_)
1879        if event.type_ == 'normal':
1880            SingleMessageWindow(account, jid,
1881                action='receive', from_whom=jid, subject=event.subject,
1882                message=event.message, resource=event.resource)
1883            app.events.remove_events(account, jid, event)
1884            return True
1885
1886        if event.type_ == 'file-request':
1887            contact = app.contacts.get_contact_with_highest_priority(account,
1888                    jid)
1889            ft.show_file_request(account, contact, event.file_props)
1890            app.events.remove_events(account, jid, event)
1891            return True
1892
1893        if event.type_ in ('file-request-error', 'file-send-error'):
1894            ft.show_send_error(event.file_props)
1895            app.events.remove_events(account, jid, event)
1896            return True
1897
1898        if event.type_ in ('file-error', 'file-stopped'):
1899            msg_err = ''
1900            if event.file_props.error == -1:
1901                msg_err = _('Remote contact stopped transfer')
1902            elif event.file_props.error == -6:
1903                msg_err = _('Error opening file')
1904            ft.show_stopped(jid, event.file_props, error_msg=msg_err)
1905            app.events.remove_events(account, jid, event)
1906            return True
1907
1908        if event.type_ == 'file-hash-error':
1909            ft.show_hash_error(jid, event.file_props, account)
1910            app.events.remove_events(account, jid, event)
1911            return True
1912
1913        if event.type_ == 'file-completed':
1914            ft.show_completed(jid, event.file_props)
1915            app.events.remove_events(account, jid, event)
1916            return True
1917
1918        if event.type_ == 'gc-invitation':
1919            open_window('GroupChatInvitation',
1920                        account=account,
1921                        event=event)
1922            app.events.remove_events(account, jid, event)
1923            return True
1924
1925        if event.type_ == 'subscription_request':
1926            open_window('SubscriptionRequest',
1927                        account=account,
1928                        jid=jid,
1929                        text=event.text,
1930                        user_nick=event.nick)
1931            app.events.remove_events(account, jid, event)
1932            return True
1933
1934        if event.type_ == 'unsubscribed':
1935            app.interface.show_unsubscribed_dialog(account, event.contact)
1936            app.events.remove_events(account, jid, event)
1937            return True
1938
1939        if event.type_ == 'jingle-incoming':
1940            ctrl = app.interface.msg_win_mgr.get_control(jid, account)
1941            if ctrl:
1942                ctrl.parent_win.set_active_tab(ctrl)
1943            else:
1944                ctrl = app.interface.new_chat_from_jid(account, jid)
1945                ctrl.add_call_received_message(event)
1946            return True
1947
1948        return False
1949
1950################################################################################
1951### This and that... random.
1952################################################################################
1953
1954    def show_roster_vbox(self, active):
1955        vb = self.xml.get_object('roster_vbox2')
1956        if active:
1957            vb.set_no_show_all(False)
1958            vb.show()
1959        else:
1960            vb.hide()
1961            vb.set_no_show_all(True)
1962
1963    def authorize(self, widget, jid, account):
1964        """
1965        Authorize a contact (by re-sending auth menuitem)
1966        """
1967        app.connections[account].get_module('Presence').subscribed(jid)
1968        InformationDialog(_('Authorization sent'),
1969            _('"%s" will now see your status.') %jid)
1970
1971    def req_sub(self, widget, jid, txt, account, groups=None, nickname=None,
1972                    auto_auth=False):
1973        """
1974        Request subscription to a contact
1975        """
1976        groups_list = groups or []
1977        app.connections[account].get_module('Presence').subscribe(
1978            jid, txt, nickname, groups_list, auto_auth)
1979        contact = app.contacts.get_contact_with_highest_priority(account, jid)
1980        if not contact:
1981            contact = app.contacts.create_contact(jid=jid, account=account,
1982                name=nickname, groups=groups_list, show='requested', status='',
1983                ask='none', sub='subscribe')
1984            app.contacts.add_contact(account, contact)
1985        else:
1986            if not _('Not in contact list') in contact.get_shown_groups():
1987                InformationDialog(_('Subscription request has been '
1988                    'sent'), _('If "%s" accepts this request you will know '
1989                    'their status.') % jid)
1990                return
1991            self.remove_contact(contact.jid, account, force=True)
1992            contact.groups = groups_list
1993            if nickname:
1994                contact.name = nickname
1995        self.add_contact(jid, account)
1996
1997    def revoke_auth(self, widget, jid, account):
1998        """
1999        Revoke a contact's authorization
2000        """
2001        app.connections[account].get_module('Presence').unsubscribed(jid)
2002        InformationDialog(_('Authorization removed'),
2003            _('Now "%s" will always see you as offline.') %jid)
2004
2005    def set_state(self, account, state):
2006        child_iterA = self._get_account_iter(account, self.model)
2007        if child_iterA:
2008            self.model[child_iterA][0] = get_icon_name(state)
2009        if app.interface.systray_enabled:
2010            app.interface.systray.change_status(state)
2011
2012    def set_connecting_state(self, account):
2013        self.set_state(account, 'connecting')
2014
2015    def send_status(self, account, status, txt):
2016        if status != 'offline':
2017            app.settings.set_account_setting(account, 'last_status', status)
2018            app.settings.set_account_setting(account, 'last_status_msg',
2019                    helpers.to_one_line(txt))
2020            if not app.account_is_available(account):
2021                self.set_connecting_state(account)
2022
2023        if status == 'offline':
2024            self.delete_pep(app.get_jid_from_account(account), account)
2025
2026        app.connections[account].change_status(status, txt)
2027        self._status_selector.update()
2028
2029    def delete_pep(self, jid, account):
2030        if jid == app.get_jid_from_account(account):
2031            app.connections[account].pep = {}
2032            self.draw_account(account)
2033
2034        for contact in app.contacts.get_contacts(account, jid):
2035            contact.pep = {}
2036
2037        self.draw_all_pep_types(jid, account)
2038        ctrl = app.interface.msg_win_mgr.get_control(jid, account)
2039        if ctrl:
2040            ctrl.update_all_pep_types()
2041
2042    def chg_contact_status(self, contact, show, status_message, account):
2043        """
2044        When a contact changes their status
2045        """
2046        contact_instances = app.contacts.get_contacts(account, contact.jid)
2047        contact.show = show
2048        contact.status = status_message
2049        # name is to show in conversation window
2050        name = contact.get_shown_name()
2051        fjid = contact.get_full_jid()
2052
2053        # The contact has several resources
2054        if len(contact_instances) > 1:
2055            if contact.resource != '':
2056                name += '/' + contact.resource
2057
2058            # Remove resource when going offline
2059            if show in ('offline', 'error') and \
2060            not self.contact_has_pending_roster_events(contact, account):
2061                ctrl = app.interface.msg_win_mgr.get_control(fjid, account)
2062                if ctrl:
2063                    ctrl.update_ui()
2064                    ctrl.parent_win.redraw_tab(ctrl)
2065                    # keep the contact around, since it's
2066                    # already attached to the control
2067                else:
2068                    app.contacts.remove_contact(account, contact)
2069
2070        elif contact.jid == app.get_jid_from_account(account) and \
2071        show in ('offline', 'error'):
2072            self.remove_contact(contact.jid, account, backend=True)
2073
2074        uf_show = helpers.get_uf_show(show)
2075
2076        # print status in chat window and update status/GPG image
2077        ctrl = app.interface.msg_win_mgr.get_control(contact.jid, account)
2078        if ctrl and not ctrl.is_groupchat:
2079            ctrl.contact = app.contacts.get_contact_with_highest_priority(
2080                account, contact.jid)
2081            ctrl.update_status_display(name, uf_show, status_message)
2082
2083        if contact.resource:
2084            ctrl = app.interface.msg_win_mgr.get_control(fjid, account)
2085            if ctrl:
2086                ctrl.update_status_display(name, uf_show, status_message)
2087
2088        # Delete pep if needed
2089        keep_pep = any(c.show not in ('error', 'offline') for c in
2090            contact_instances)
2091        if not keep_pep and contact.jid != app.get_jid_from_account(account) \
2092        and not contact.is_groupchat:
2093            self.delete_pep(contact.jid, account)
2094
2095        # Redraw everything and select the sender
2096        self.adjust_and_draw_contact_context(contact.jid, account)
2097
2098
2099    def on_status_changed(self, account, show):
2100        """
2101        The core tells us that our status has changed
2102        """
2103        if account not in app.contacts.get_accounts():
2104            return
2105        child_iterA = self._get_account_iter(account, self.model)
2106        self_resource = app.connections[account].get_own_jid().resource
2107        self_contact = app.contacts.get_contact(account,
2108                app.get_jid_from_account(account), resource=self_resource)
2109        if self_contact:
2110            status_message = app.connections[account].status_message
2111            self.chg_contact_status(self_contact, show, status_message, account)
2112        self.set_account_status_icon(account)
2113        if show == 'offline':
2114            if self.quit_on_next_offline > -1:
2115                # we want to quit, we are waiting for all accounts to be offline
2116                self.quit_on_next_offline -= 1
2117                if self.quit_on_next_offline < 1:
2118                    # all accounts offline, quit
2119                    self.quit_gtkgui_interface()
2120            else:
2121                # No need to redraw contacts if we're quitting
2122                if child_iterA:
2123                    self.model[child_iterA][Column.AVATAR_IMG] = None
2124                for jid in list(app.contacts.get_jid_list(account)):
2125                    lcontact = app.contacts.get_contacts(account, jid)
2126                    ctrl = app.interface.msg_win_mgr.get_gc_control(jid,
2127                        account)
2128                    for contact in [c for c in lcontact if (
2129                    (c.show != 'offline' or c.is_transport()) and not ctrl)]:
2130                        self.chg_contact_status(contact, 'offline', '', account)
2131        if app.interface.systray_enabled:
2132            app.interface.systray.change_status(show)
2133        self._status_selector.update()
2134
2135    def change_status(self, _widget, account, status):
2136        app.interface.change_account_status(account, status=status)
2137
2138    def get_show(self, lcontact):
2139        prio = lcontact[0].priority
2140        show = lcontact[0].show
2141        for u in lcontact:
2142            if u.priority > prio:
2143                prio = u.priority
2144                show = u.show
2145        return show
2146
2147    def on_message_window_delete(self, win_mgr, msg_win):
2148        if app.settings.get('one_message_window') == 'always_with_roster':
2149            self.show_roster_vbox(True)
2150            resize_window(self.window,
2151                          app.settings.get('roster_width'),
2152                          app.settings.get('roster_height'))
2153
2154    def close_all_from_dict(self, dic):
2155        """
2156        Close all the windows in the given dictionary
2157        """
2158        for w in list(dic.values()):
2159            if isinstance(w, dict):
2160                self.close_all_from_dict(w)
2161            else:
2162                try:
2163                    w.window.destroy()
2164                except (AttributeError, RuntimeError):
2165                    w.destroy()
2166
2167    def close_all(self, account, force=False):
2168        """
2169        Close all the windows from an account. If force is True, do not ask
2170        confirmation before closing chat/gc windows
2171        """
2172        if account in app.interface.instances:
2173            self.close_all_from_dict(app.interface.instances[account])
2174        for ctrl in app.interface.msg_win_mgr.get_controls(acct=account):
2175            ctrl.parent_win.remove_tab(ctrl, ctrl.parent_win.CLOSE_CLOSE_BUTTON,
2176                force=force)
2177
2178    def on_roster_window_delete_event(self, widget, event):
2179        """
2180        Main window X button was clicked
2181        """
2182        if not app.settings.get('quit_on_roster_x_button') and (
2183        (app.interface.systray_enabled and app.settings.get('trayicon') != \
2184        'on_event') or app.settings.get('allow_hide_roster')):
2185            save_roster_position(self.window)
2186            if os.name == 'nt' or app.settings.get('hide_on_roster_x_button'):
2187                self.window.hide()
2188            else:
2189                self.window.iconify()
2190        elif app.settings.get('quit_on_roster_x_button'):
2191            self.on_quit_request()
2192        else:
2193            def _on_ok(is_checked):
2194                if is_checked:
2195                    app.settings.set('quit_on_roster_x_button', True)
2196                self.on_quit_request()
2197            ConfirmationCheckDialog(
2198                _('Quit Gajim'),
2199                _('You are about to quit Gajim'),
2200                _('Are you sure you want to quit Gajim?'),
2201                _('_Always quit when closing Gajim'),
2202                [DialogButton.make('Cancel'),
2203                 DialogButton.make('Remove',
2204                                   text=_('_Quit'),
2205                                   callback=_on_ok)]).show()
2206        return True #  Do NOT destroy the window
2207
2208    def prepare_quit(self):
2209        if self.save_done:
2210            return
2211        msgwin_width_adjust = 0
2212
2213        # in case show_roster_on_start is False and roster is never shown
2214        # window.window is None
2215        if self.window.get_window() is not None:
2216            save_roster_position(self.window)
2217            width, height = self.window.get_size()
2218            app.settings.set('roster_width', width)
2219            app.settings.set('roster_height', height)
2220            if not self.xml.get_object('roster_vbox2').get_property('visible'):
2221                # The roster vbox is hidden, so the message window is larger
2222                # then we want to save (i.e. the window will grow every startup)
2223                # so adjust.
2224                msgwin_width_adjust = -1 * width
2225        app.settings.set('last_roster_visible',
2226                self.window.get_property('visible'))
2227        app.interface.msg_win_mgr.save_opened_controls()
2228        app.interface.msg_win_mgr.shutdown(msgwin_width_adjust)
2229
2230        app.settings.set('collapsed_rows', '\t'.join(self.collapsed_rows))
2231        app.interface.save_config()
2232        for account in app.connections:
2233            app.connections[account].quit(True)
2234            self.close_all(account)
2235        if app.interface.systray_enabled:
2236            app.interface.hide_systray()
2237        self.save_done = True
2238
2239    def quit_gtkgui_interface(self):
2240        """
2241        When we quit the gtk interface - exit gtk
2242        """
2243        self.prepare_quit()
2244        self.application.quit()
2245
2246    def on_quit_request(self, widget=None):
2247        """
2248        User wants to quit. Check if he should be warned about messages pending.
2249        Terminate all sessions and send offline to all connected account. We do
2250        NOT really quit gajim here
2251        """
2252        accounts = list(app.connections.keys())
2253        get_msg = False
2254        for acct in accounts:
2255            if app.account_is_available(acct):
2256                get_msg = True
2257                break
2258
2259        def on_continue3(message):
2260            self.quit_on_next_offline = 0
2261            accounts_to_disconnect = []
2262            for acct in accounts:
2263                if app.account_is_available(acct):
2264                    self.quit_on_next_offline += 1
2265                    accounts_to_disconnect.append(acct)
2266
2267            if not self.quit_on_next_offline:
2268                # all accounts offline, quit
2269                self.quit_gtkgui_interface()
2270                return
2271
2272            for acct in accounts_to_disconnect:
2273                self.send_status(acct, 'offline', message)
2274
2275        def on_continue2(message):
2276            if 'file_transfers' not in app.interface.instances:
2277                on_continue3(message)
2278                return
2279            # check if there is an active file transfer
2280            from gajim.common.modules.bytestream import is_transfer_active
2281            files_props = app.interface.instances['file_transfers'].\
2282                files_props
2283            transfer_active = False
2284            for x in files_props:
2285                for y in files_props[x]:
2286                    if is_transfer_active(files_props[x][y]):
2287                        transfer_active = True
2288                        break
2289
2290            if transfer_active:
2291                ConfirmationDialog(
2292                    _('Stop File Transfers'),
2293                    _('You still have running file transfers'),
2294                    _('If you quit now, the file(s) being transferred will '
2295                      'be lost.\n'
2296                      'Do you still want to quit?'),
2297                    [DialogButton.make('Cancel'),
2298                     DialogButton.make('Remove',
2299                                       text=_('_Quit'),
2300                                       callback=on_continue3,
2301                                       args=[message])]).show()
2302                return
2303            on_continue3(message)
2304
2305        def on_continue(message):
2306            if message is None:
2307                # user pressed Cancel to change status message dialog
2308                return
2309            # check if we have unread messages
2310            unread = app.events.get_nb_events()
2311
2312            for event in app.events.get_all_events(['printed_gc_msg']):
2313                contact = app.contacts.get_groupchat_contact(event.account,
2314                                                             event.jid)
2315                if contact is None or not contact.can_notify():
2316                    unread -= 1
2317
2318            # check if we have recent messages
2319            recent = False
2320            for win in app.interface.msg_win_mgr.windows():
2321                for ctrl in win.controls():
2322                    fjid = ctrl.get_full_jid()
2323                    if fjid in app.last_message_time[ctrl.account]:
2324                        if time.time() - app.last_message_time[ctrl.account][
2325                        fjid] < 2:
2326                            recent = True
2327                            break
2328                if recent:
2329                    break
2330
2331            if unread or recent:
2332                ConfirmationDialog(
2333                    _('Unread Messages'),
2334                    _('You still have unread messages'),
2335                    _('Messages will only be available for reading them later '
2336                      'if storing chat history is enabled and if the contact '
2337                      'is in your contact list.'),
2338                    [DialogButton.make('Cancel'),
2339                     DialogButton.make('Remove',
2340                                       text=_('_Quit'),
2341                                       callback=on_continue2,
2342                                       args=[message])]).show()
2343                return
2344            on_continue2(message)
2345
2346        if get_msg and ask_for_status_message('offline'):
2347            open_window('StatusChange',
2348                        status='offline',
2349                        callback=on_continue,
2350                        show_pep=False)
2351        else:
2352            on_continue('')
2353
2354    def _nec_presence_received(self, obj):
2355        account = obj.conn.name
2356        jid = obj.jid
2357
2358        if obj.need_add_in_roster:
2359            self.add_contact(jid, account)
2360
2361        jid_list = app.contacts.get_jid_list(account)
2362        if jid in jid_list or jid == app.get_jid_from_account(account):
2363            if not app.jid_is_transport(jid) and len(obj.contact_list) == 1:
2364                if obj.old_show == 0 and obj.new_show > 1:
2365                    GLib.timeout_add_seconds(5, self.remove_newly_added, jid,
2366                        account)
2367                elif obj.old_show > 1 and obj.new_show == 0 and \
2368                obj.conn.state.is_available:
2369                    GLib.timeout_add_seconds(5, self.remove_to_be_removed,
2370                        jid, account)
2371
2372        self.draw_contact(jid, account)
2373
2374        if app.jid_is_transport(jid) and jid in jid_list:
2375            # It must be an agent
2376            # Update existing iter and group counting
2377            self.draw_contact(jid, account)
2378            self.draw_group(_('Transports'), account)
2379
2380        if obj.contact:
2381            self.chg_contact_status(obj.contact, obj.show, obj.status, account)
2382
2383        if obj.popup:
2384            ctrl = app.interface.msg_win_mgr.search_control(jid, account)
2385            if ctrl:
2386                GLib.idle_add(ctrl.parent_win.set_active_tab, ctrl)
2387            else:
2388                ctrl = app.interface.new_chat(obj.contact, account)
2389                if app.events.get_events(account, obj.jid):
2390                    ctrl.read_queue()
2391
2392    def _nec_roster_received(self, obj):
2393        if obj.received_from_server:
2394            self.fill_contacts_and_groups_dicts(obj.roster, obj.conn.name)
2395            self.add_account_contacts(obj.conn.name)
2396            self.fire_up_unread_messages_events(obj.conn.name)
2397        else:
2398            # add self contact
2399            account = obj.conn.name
2400            self_jid = app.get_jid_from_account(account)
2401            if self_jid not in app.contacts.get_jid_list(account):
2402                sha = app.settings.get_account_setting(account, 'avatar_sha')
2403                contact = app.contacts.create_contact(
2404                    jid=self_jid, account=account, name=app.nicks[account],
2405                    groups=['self_contact'], show='offline', sub='both',
2406                    ask='none', avatar_sha=sha)
2407                app.contacts.add_contact(account, contact)
2408                self.add_contact(self_jid, account)
2409
2410            if app.settings.get('remember_opened_chat_controls'):
2411                account = obj.conn.name
2412                controls = app.settings.get_account_setting(
2413                    account, 'opened_chat_controls')
2414                if controls:
2415                    for jid in controls.split(','):
2416                        contact = \
2417                            app.contacts.get_contact_with_highest_priority(
2418                                account, jid)
2419                        if not contact:
2420                            contact = self.add_to_not_in_the_roster(
2421                                account, jid)
2422                        app.interface.on_open_chat_window(
2423                            None, contact, account)
2424                app.settings.set_account_setting(
2425                    account, 'opened_chat_controls', '')
2426            GLib.idle_add(self.refilter_shown_roster_items)
2427
2428    def _nec_anonymous_auth(self, obj):
2429        """
2430        This event is raised when our JID changed (most probably because we use
2431        anonymous account. We update contact and roster entry in this case
2432        """
2433        self.rename_self_contact(obj.old_jid, obj.new_jid, obj.conn.name)
2434
2435    def _nec_our_show(self, event):
2436        if event.show == 'offline':
2437            self.application.set_account_actions_state(event.account)
2438            self.application.update_app_actions_state()
2439
2440        self.on_status_changed(event.account, event.show)
2441
2442    def _nec_connection_type(self, obj):
2443        self.draw_account(obj.conn.name)
2444
2445    def _nec_agent_removed(self, obj):
2446        for jid in obj.jid_list:
2447            self.remove_contact(jid, obj.conn.name, backend=True)
2448
2449    def _on_mood_received(self, event):
2450        if event.is_self_message:
2451            self.draw_account(event.account)
2452        self._draw_pep(event.account, event.jid, PEPEventType.MOOD)
2453
2454    def _on_activity_received(self, event):
2455        if event.is_self_message:
2456            self.draw_account(event.account)
2457        self._draw_pep(event.account, event.jid, PEPEventType.ACTIVITY)
2458
2459    def _on_tune_received(self, event):
2460        if event.is_self_message:
2461            self.draw_account(event.account)
2462        self._draw_pep(event.account, event.jid, PEPEventType.TUNE)
2463
2464    def _on_location_received(self, event):
2465        if event.is_self_message:
2466            self.draw_account(event.account)
2467        self._draw_pep(event.account, event.jid, PEPEventType.LOCATION)
2468
2469    def _on_nickname_received(self, event):
2470        self.draw_contact(event.jid, event.account)
2471
2472    def _nec_update_avatar(self, obj):
2473        app.log('avatar').debug('Draw roster avatar: %s', obj.jid)
2474        self.draw_avatar(obj.jid, obj.account)
2475
2476    def _nec_muc_subject_received(self, event):
2477        self.draw_contact(event.room_jid, event.account)
2478
2479    def _on_muc_disco_update(self, event):
2480        self.draw_contact(str(event.room_jid), event.account)
2481
2482    def _on_bookmarks_received(self, event):
2483        con = app.connections[event.account]
2484        for bookmark in con.get_module('Bookmarks').bookmarks:
2485            self.draw_contact(str(bookmark.jid), event.account)
2486
2487    def _nec_metacontacts_received(self, obj):
2488        self.redraw_metacontacts(obj.conn.name)
2489
2490    def _nec_signed_in(self, obj):
2491        self.application.set_account_actions_state(obj.conn.name, True)
2492        self.application.update_app_actions_state()
2493        self.draw_account(obj.conn.name)
2494
2495    def _nec_decrypted_message_received(self, obj):
2496        if not obj.msgtxt:
2497            return True
2498        if obj.properties.type.value not in ('normal', 'chat'):
2499            return
2500
2501        if obj.popup and not obj.session.control:
2502            contact = app.contacts.get_contact(obj.conn.name, obj.jid)
2503            obj.session.control = app.interface.new_chat(contact,
2504                obj.conn.name, session=obj.session)
2505            if app.events.get_events(obj.conn.name, obj.fjid):
2506                obj.session.control.read_queue()
2507
2508        if not obj.properties.is_muc_pm and obj.show_in_roster:
2509            self.draw_contact(obj.jid, obj.conn.name)
2510            self.show_title() # we show the * or [n]
2511            # Select the big brother contact in roster, it's visible because it
2512            # has events.
2513            family = app.contacts.get_metacontacts_family(obj.conn.name,
2514                obj.jid)
2515            if family:
2516                _nearby_family, bb_jid, bb_account = \
2517                    app.contacts.get_nearby_family_and_big_brother(family,
2518                    obj.conn.name)
2519            else:
2520                bb_jid, bb_account = obj.jid, obj.conn.name
2521            self.select_contact(bb_jid, bb_account)
2522
2523################################################################################
2524### Menu and GUI callbacks
2525### FIXME: order callbacks in itself...
2526################################################################################
2527
2528    def on_info(self, widget, contact, account):
2529        """
2530        Call vcard_information_window class to display contact's information
2531        """
2532        if app.connections[account].is_zeroconf:
2533            self.on_info_zeroconf(widget, contact, account)
2534            return
2535
2536        info = app.interface.instances[account]['infos']
2537        if contact.jid in info:
2538            info[contact.jid].window.present()
2539        else:
2540            info[contact.jid] = vcard.VcardWindow(contact, account)
2541
2542    def on_info_zeroconf(self, widget, contact, account):
2543        info = app.interface.instances[account]['infos']
2544        if contact.jid in info:
2545            info[contact.jid].window.present()
2546        else:
2547            contact = app.contacts.get_first_contact_from_jid(account,
2548                                            contact.jid)
2549            if contact.show in ('offline', 'error'):
2550                # don't show info on offline contacts
2551                return
2552            info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account)
2553
2554    def on_edit_agent(self, widget, contact, account):
2555        """
2556        When we want to modify the agent registration
2557        """
2558        ServiceRegistration(account, contact.jid)
2559
2560    def on_remove_agent(self, widget, list_):
2561        """
2562        When an agent is requested to be removed. list_ is a list of (contact,
2563        account) tuple
2564        """
2565        for (contact, account) in list_:
2566            if app.settings.get_account_setting(account, 'hostname') == \
2567            contact.jid:
2568                # We remove the server contact
2569                # remove it from treeview
2570                app.connections[account].get_module('Presence').unsubscribe(contact.jid)
2571                self.remove_contact(contact.jid, account, backend=True)
2572                return
2573
2574        def remove():
2575            for (contact, account) in list_:
2576                full_jid = contact.get_full_jid()
2577                app.connections[account].get_module('Gateway').unsubscribe(full_jid)
2578                # remove transport from treeview
2579                self.remove_contact(contact.jid, account, backend=True)
2580
2581        # Check if there are unread events from some contacts
2582        has_unread_events = False
2583        for (contact, account) in list_:
2584            for jid in app.events.get_events(account):
2585                if jid.endswith(contact.jid):
2586                    has_unread_events = True
2587                    break
2588        if has_unread_events:
2589            ErrorDialog(
2590                _('You have unread messages'),
2591                _('You must read them before removing this transport.'))
2592            return
2593        if len(list_) == 1:
2594            pritext = _('Transport \'%s\' will be removed') % list_[0][0].jid
2595            sectext = _('You will no longer be able to send and receive '
2596                        'messages from and to contacts using this transport.')
2597        else:
2598            pritext = _('Transports will be removed')
2599            jids = ''
2600            for (contact, account) in list_:
2601                jids += '\n  ' + contact.get_shown_name() + ','
2602            jids = jids[:-1] + '.'
2603            sectext = _('You will no longer be able to send and receive '
2604                        'messages from and to contacts using these '
2605                        'transports:\n%s') % jids
2606        ConfirmationDialog(
2607            _('Remove Transport'),
2608            pritext,
2609            sectext,
2610            [DialogButton.make('Cancel'),
2611             DialogButton.make('Remove',
2612                               callback=remove)],
2613            transient_for=self.window).show()
2614
2615    def _nec_blocking(self, obj):
2616        for jid in obj.changed:
2617            self.draw_contact(str(jid), obj.conn.name)
2618
2619    def on_block(self, widget, list_):
2620        """
2621        When clicked on the 'block' button in context menu. list_ is a list of
2622        (contact, account)
2623        """
2624        def _block_it(is_checked=None, report=None):
2625            if is_checked is not None:  # Dialog has been shown
2626                if is_checked:
2627                    app.settings.set('confirm_block', 'no')
2628                else:
2629                    app.settings.set('confirm_block', 'yes')
2630
2631            accounts = []
2632            for _, account in list_:
2633                con = app.connections[account]
2634                if con.get_module('Blocking').supported:
2635                    accounts.append(account)
2636
2637            for acct in accounts:
2638                l_ = [i[0] for i in list_ if i[1] == acct]
2639                con = app.connections[acct]
2640                jid_list = [contact.jid for contact in l_]
2641                con.get_module('Blocking').block(jid_list, report)
2642                for contact in l_:
2643                    app.events.remove_events(acct, contact.jid)
2644                    ctrl = app.interface.msg_win_mgr.get_control(
2645                        contact.jid, acct)
2646                    if ctrl:
2647                        ctrl.parent_win.remove_tab(
2648                            ctrl, ctrl.parent_win.CLOSE_COMMAND, force=True)
2649                    if contact.show == 'not in roster':
2650                        self.remove_contact(contact.jid, acct, force=True,
2651                                            backend=True)
2652                        return
2653                    self.draw_contact(contact.jid, acct)
2654
2655        # Check if confirmation is needed for blocking
2656        confirm_block = app.settings.get('confirm_block')
2657        if confirm_block == 'no':
2658            _block_it()
2659            return
2660
2661        ConfirmationCheckDialog(
2662            _('Block Contact'),
2663            _('Really block this contact?'),
2664            _('You will appear offline for this contact and you '
2665              'will not receive further messages.'),
2666            _('_Do not ask again'),
2667            [DialogButton.make('Cancel'),
2668             DialogButton.make('OK',
2669                               text=_('_Report Spam'),
2670                               callback=_block_it,
2671                               kwargs={'report': 'spam'}),
2672             DialogButton.make('Remove',
2673                               text=_('_Block'),
2674                               callback=_block_it)],
2675            modal=False).show()
2676
2677    def on_unblock(self, widget, list_):
2678        """
2679        When clicked on the 'unblock' button in context menu.
2680        """
2681        accounts = []
2682        for _, account in list_:
2683            con = app.connections[account]
2684            if con.get_module('Blocking').supported:
2685                accounts.append(account)
2686
2687        for acct in accounts:
2688            l_ = [i[0] for i in list_ if i[1] == acct]
2689            con = app.connections[acct]
2690            jid_list = [contact.jid for contact in l_]
2691            con.get_module('Blocking').unblock(jid_list)
2692            for contact in l_:
2693                self.draw_contact(contact.jid, acct)
2694
2695    def on_rename(self, widget, row_type, jid, account):
2696        # This function is called either by F2 or by Rename menuitem
2697        if 'rename' in app.interface.instances:
2698            app.interface.instances['rename'].dialog.present()
2699            return
2700
2701        # Account is offline, don't allow to rename
2702        if not app.account_is_available(account):
2703            return
2704        if row_type in ('contact', 'agent'):
2705            # It's jid
2706            title = _('Rename Contact')
2707            text = _('Rename contact %s?') % jid
2708            sec_text = _('Please enter a new nickname')
2709            old_text = app.contacts.get_contact_with_highest_priority(account,
2710                                                                      jid).name
2711        elif row_type == 'group':
2712            if jid in helpers.special_groups + (_('General'),):
2713                return
2714            old_text = jid
2715            title = _('Rename Group')
2716            text = _('Rename group %s?') % GLib.markup_escape_text(jid)
2717            sec_text = _('Please enter a new name')
2718
2719        def _on_renamed(new_text, account, row_type, jid, old_text):
2720            if row_type in ('contact', 'agent'):
2721                if old_text == new_text:
2722                    return
2723                contacts = app.contacts.get_contacts(account, jid)
2724                for contact in contacts:
2725                    contact.name = new_text
2726                con = app.connections[account]
2727                con.get_module('Roster').update_contact(
2728                    jid, new_text, contacts[0].groups)
2729                self.draw_contact(jid, account)
2730                # Update opened chats
2731                for ctrl in app.interface.msg_win_mgr.get_controls(jid,
2732                account):
2733                    ctrl.update_ui()
2734                    win = app.interface.msg_win_mgr.get_window(jid, account)
2735                    win.redraw_tab(ctrl)
2736                    win.show_title()
2737            elif row_type == 'group':
2738                # In Column.JID column, we hold the group name (which is not escaped)
2739                self.rename_group(old_text, new_text, account)
2740
2741        InputDialog(
2742            title,
2743            text,
2744            sec_text,
2745            [DialogButton.make('Cancel'),
2746             DialogButton.make('Accept',
2747                               text=_('_Rename'),
2748                               callback=_on_renamed,
2749                               args=[account,
2750                                     row_type,
2751                                     jid,
2752                                     old_text])],
2753            input_str=old_text,
2754            transient_for=self.window).show()
2755
2756    def on_remove_group_item_activated(self, widget, group, account):
2757        def _on_ok(is_checked):
2758            for contact in app.contacts.get_contacts_from_group(account,
2759            group):
2760                if not is_checked:
2761                    self.remove_contact_from_groups(contact.jid, account,
2762                        [group])
2763                else:
2764                    app.connections[account].get_module(
2765                        'Presence').unsubscribe(contact.jid)
2766                    self.remove_contact(contact.jid, account, backend=True)
2767
2768        ConfirmationCheckDialog(
2769            _('Remove Group'),
2770            _('Remove Group'),
2771            _('Do you want to remove %s from the contact list?') % group,
2772            _('_Also remove all contacts of this group from contact list'),
2773            [DialogButton.make('Cancel'),
2774             DialogButton.make('Remove',
2775                               callback=_on_ok)]).show()
2776
2777    def on_edit_groups(self, widget, list_):
2778        dialogs.EditGroupsDialog(list_)
2779
2780    def on_disconnect(self, widget, jid, account):
2781        """
2782        When disconnect menuitem is activated: disconnect from room
2783        """
2784        if jid in app.interface.minimized_controls[account]:
2785            ctrl = app.interface.minimized_controls[account][jid]
2786            ctrl.leave()
2787        self.remove_groupchat(jid, account)
2788
2789    def on_send_single_message_menuitem_activate(self, widget, account,
2790    contact=None):
2791        if contact is None:
2792            SingleMessageWindow(account, action='send')
2793        elif isinstance(contact, list):
2794            SingleMessageWindow(account, contact, 'send')
2795        else:
2796            jid = contact.jid
2797            if contact.jid == app.get_jid_from_account(account):
2798                jid += '/' + contact.resource
2799            SingleMessageWindow(account, jid, 'send')
2800
2801    def on_send_file_menuitem_activate(self, widget, contact, account,
2802    resource=None):
2803        app.interface.instances['file_transfers'].show_file_send_request(
2804            account, contact)
2805
2806    def on_invite_to_room(self,
2807                          _widget,
2808                          list_,
2809                          room_jid,
2810                          room_account,
2811                          resource=None):
2812        """
2813        Resource parameter MUST NOT be used if more than one contact in list
2814        """
2815        gc_control = app.get_groupchat_control(room_account, room_jid)
2816        if gc_control is None:
2817            return
2818
2819        for contact, _ in list_:
2820            contact_jid = contact.jid
2821            if resource: # we MUST have one contact only in list_
2822                contact_jid += '/' + resource
2823            gc_control.invite(contact_jid)
2824
2825    def on_all_groupchat_maximized(self, widget, group_list):
2826        for (contact, account) in group_list:
2827            self.on_groupchat_maximized(widget, contact.jid, account)
2828
2829    def on_groupchat_maximized(self, widget, jid, account):
2830        """
2831        When a groupchat is maximized
2832        """
2833        if not jid in app.interface.minimized_controls[account]:
2834            # Already opened?
2835            gc_control = app.interface.msg_win_mgr.get_gc_control(jid,
2836                account)
2837            if gc_control:
2838                mw = app.interface.msg_win_mgr.get_window(jid, account)
2839                mw.set_active_tab(gc_control)
2840            return
2841        ctrl = app.interface.minimized_controls[account][jid]
2842        mw = app.interface.msg_win_mgr.get_window(jid, account)
2843        if not mw:
2844            mw = app.interface.msg_win_mgr.create_window(
2845                ctrl.contact, ctrl.account, ctrl.type)
2846            id_ = mw.window.connect('motion-notify-event',
2847                ctrl._on_window_motion_notify)
2848            ctrl.handlers[id_] = mw.window
2849        ctrl.parent_win = mw
2850        ctrl.on_groupchat_maximize()
2851        mw.new_tab(ctrl)
2852        mw.set_active_tab(ctrl)
2853        self.remove_groupchat(jid, account, maximize=True)
2854
2855    def on_groupchat_rename(self, _widget, jid, account):
2856        def _on_rename(new_name):
2857            con = app.connections[account]
2858            con.get_module('Bookmarks').modify(jid, name=new_name)
2859
2860        contact = app.contacts.get_first_contact_from_jid(account, jid)
2861        name = contact.get_shown_name()
2862
2863        InputDialog(
2864            _('Rename Group Chat'),
2865            _('Rename Group Chat'),
2866            _('Please enter a new name for this group chat'),
2867            [DialogButton.make('Cancel'),
2868             DialogButton.make('Accept',
2869                               text=_('_Rename'),
2870                               callback=_on_rename)],
2871            input_str=name,
2872            transient_for=self.window).show()
2873
2874    def on_change_status_message_activate(self, _widget, account):
2875        app.interface.change_account_status(account)
2876
2877    def on_add_to_roster(self, widget, contact, account):
2878        AddNewContactWindow(account, contact.jid, contact.name)
2879
2880    def on_roster_treeview_key_press_event(self, widget, event):
2881        """
2882        When a key is pressed in the treeviews
2883        """
2884        if event.keyval == Gdk.KEY_Escape:
2885            if self.rfilter_enabled:
2886                self.disable_rfilter()
2887            else:
2888                self.tree.get_selection().unselect_all()
2889        elif event.keyval == Gdk.KEY_F2:
2890            treeselection = self.tree.get_selection()
2891            model, list_of_paths = treeselection.get_selected_rows()
2892            if len(list_of_paths) != 1:
2893                return
2894            path = list_of_paths[0]
2895            type_ = model[path][Column.TYPE]
2896            if type_ in ('contact', 'group', 'agent'):
2897                jid = model[path][Column.JID]
2898                account = model[path][Column.ACCOUNT]
2899                self.on_rename(widget, type_, jid, account)
2900
2901        elif event.keyval == Gdk.KEY_Delete:
2902            treeselection = self.tree.get_selection()
2903            model, list_of_paths = treeselection.get_selected_rows()
2904            if not list_of_paths:
2905                return
2906            type_ = model[list_of_paths[0]][Column.TYPE]
2907            account = model[list_of_paths[0]][Column.ACCOUNT]
2908            if type_ in ('account', 'group', 'self_contact') or \
2909            account == app.ZEROCONF_ACC_NAME:
2910                return
2911            list_ = []
2912            for path in list_of_paths:
2913                if model[path][Column.TYPE] != type_:
2914                    return
2915                jid = model[path][Column.JID]
2916                account = model[path][Column.ACCOUNT]
2917                if not app.account_is_available(account):
2918                    continue
2919                contact = app.contacts.get_contact_with_highest_priority(
2920                    account, jid)
2921                list_.append((contact, account))
2922            if not list_:
2923                return
2924            if type_ == 'contact':
2925                self.on_req_usub(widget, list_)
2926            elif type_ == 'agent':
2927                self.on_remove_agent(widget, list_)
2928
2929        elif not (event.get_state() &
2930                  (Gdk.ModifierType.CONTROL_MASK |
2931                   Gdk.ModifierType.MOD1_MASK)):
2932            num = Gdk.keyval_to_unicode(event.keyval)
2933            if num and num > 31:
2934                # if we got unicode symbol without ctrl / alt
2935                self.enable_rfilter(chr(num))
2936
2937        elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and \
2938        event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
2939        event.keyval == Gdk.KEY_U:
2940            self.enable_rfilter('')
2941            self.rfilter_entry.event(event)
2942
2943        elif event.keyval == Gdk.KEY_Left:
2944            treeselection = self.tree.get_selection()
2945            model, list_of_paths = treeselection.get_selected_rows()
2946            if len(list_of_paths) != 1:
2947                return
2948            path = list_of_paths[0]
2949            iter_ = model.get_iter(path)
2950            if model.iter_has_child(iter_) and self.tree.row_expanded(path):
2951                self.tree.collapse_row(path)
2952                return True
2953            if path.get_depth() > 1:
2954                self.tree.set_cursor(path[:-1])
2955                return True
2956        elif event.keyval == Gdk.KEY_Right:
2957            treeselection = self.tree.get_selection()
2958            model, list_of_paths = treeselection.get_selected_rows()
2959            if len(list_of_paths) != 1:
2960                return
2961            path = list_of_paths[0]
2962            iter_ = model.get_iter(path)
2963            if model.iter_has_child(iter_):
2964                self.tree.expand_row(path, False)
2965                return True
2966
2967    def accel_group_func(self, accel_group, acceleratable, keyval, modifier):
2968        # CTRL mask
2969        if modifier & Gdk.ModifierType.CONTROL_MASK:
2970            if keyval == Gdk.KEY_s:  # CTRL + s
2971                app.interface.change_status()
2972                return True
2973            if keyval == Gdk.KEY_k:  # CTRL + k
2974                self.enable_rfilter('')
2975
2976    def on_roster_treeview_button_press_event(self, widget, event):
2977        try:
2978            pos = self.tree.get_path_at_pos(int(event.x), int(event.y))
2979            path, x = pos[0], pos[2]
2980        except TypeError:
2981            self.tree.get_selection().unselect_all()
2982            return False
2983
2984        if event.button == 3: # Right click
2985            try:
2986                model, list_of_paths = self.tree.get_selection().\
2987                    get_selected_rows()
2988            except TypeError:
2989                list_of_paths = []
2990            if path not in list_of_paths:
2991                self.tree.get_selection().unselect_all()
2992                self.tree.get_selection().select_path(path)
2993            return self.show_treeview_menu(event)
2994
2995        if event.button == 2: # Middle click
2996            try:
2997                model, list_of_paths = self.tree.get_selection().\
2998                    get_selected_rows()
2999            except TypeError:
3000                list_of_paths = []
3001            if list_of_paths != [path]:
3002                self.tree.get_selection().unselect_all()
3003                self.tree.get_selection().select_path(path)
3004            type_ = model[path][Column.TYPE]
3005            if type_ in ('agent', 'contact', 'self_contact', 'groupchat'):
3006                self.on_row_activated(widget, path)
3007            elif type_ == 'account':
3008                account = model[path][Column.ACCOUNT]
3009                if account != 'all':
3010                    if app.account_is_available(account):
3011                        app.interface.change_account_status(account)
3012                    return True
3013
3014                show = helpers.get_global_show()
3015                if show == 'offline':
3016                    return True
3017                app.interface.change_status()
3018            return True
3019
3020        if event.button == 1: # Left click
3021            model = self.modelfilter
3022            type_ = model[path][Column.TYPE]
3023            # x_min is the x start position of status icon column
3024            if app.settings.get('avatar_position_in_roster') == 'left':
3025                x_min = AvatarSize.ROSTER
3026            else:
3027                x_min = 0
3028
3029            if type_ == 'group' and x < 27:
3030                # first cell in 1st column (the arrow SINGLE clicked)
3031                if self.tree.row_expanded(path):
3032                    self.tree.collapse_row(path)
3033                else:
3034                    self.expand_group_row(path)
3035
3036            elif type_ == 'contact' and x_min < x < x_min + 27:
3037                if self.tree.row_expanded(path):
3038                    self.tree.collapse_row(path)
3039                else:
3040                    self.tree.expand_row(path, False)
3041
3042    def expand_group_row(self, path):
3043        self.tree.expand_row(path, False)
3044        iter_ = self.modelfilter.get_iter(path)
3045        child_iter = self.modelfilter.iter_children(iter_)
3046        while child_iter:
3047            type_ = self.modelfilter[child_iter][Column.TYPE]
3048            account = self.modelfilter[child_iter][Column.ACCOUNT]
3049            group = self.modelfilter[child_iter][Column.JID]
3050            if type_ == 'group' and account + group not in self.collapsed_rows:
3051                self.expand_group_row(self.modelfilter.get_path(child_iter))
3052            child_iter = self.modelfilter.iter_next(child_iter)
3053
3054    def on_req_usub(self, widget, list_):
3055        """
3056        Remove a contact. list_ is a list of (contact, account) tuples
3057        """
3058        def on_ok(is_checked):
3059            remove_auth = True
3060            if len(list_) == 1:
3061                contact = list_[0][0]
3062                if contact.sub != 'to' and is_checked:
3063                    remove_auth = False
3064            for (contact, account) in list_:
3065                if _('Not in contact list') not in contact.get_shown_groups():
3066                    app.connections[account].get_module('Presence').unsubscribe(contact.jid,
3067                        remove_auth)
3068                self.remove_contact(contact.jid, account, backend=True)
3069                if not remove_auth and contact.sub == 'both':
3070                    contact.name = ''
3071                    contact.groups = []
3072                    contact.sub = 'from'
3073                    # we can't see him, but have to set it manually in contact
3074                    contact.show = 'offline'
3075                    app.contacts.add_contact(account, contact)
3076                    self.add_contact(contact.jid, account)
3077        def on_ok2():
3078            on_ok(False)
3079
3080        if len(list_) == 1:
3081            contact = list_[0][0]
3082            title = _('Remove Contact')
3083            pritext = _('Remove contact from contact list')
3084            sectext = _('You are about to remove %(name)s (%(jid)s) from '
3085                        'your contact list.\n') % {
3086                            'name': contact.get_shown_name(),
3087                            'jid': contact.jid}
3088            if contact.sub == 'to':
3089                ConfirmationDialog(
3090                    title,
3091                    pritext,
3092                    sectext + \
3093                    _('By removing this contact you also remove authorization. '
3094                      'This means the contact will see you as offline.'),
3095                    [DialogButton.make('Cancel'),
3096                     DialogButton.make('Remove',
3097                                       callback=on_ok2)]).show()
3098            elif _('Not in contact list') in contact.get_shown_groups():
3099                # Contact is not in roster
3100                ConfirmationDialog(
3101                    title,
3102                    pritext,
3103                    sectext + \
3104                    _('Do you want to continue?'),
3105                    [DialogButton.make('Cancel'),
3106                     DialogButton.make('Remove',
3107                                       callback=on_ok2)]).show()
3108            else:
3109                ConfirmationCheckDialog(
3110                    title,
3111                    pritext,
3112                    sectext + \
3113                    _('By removing this contact you also remove authorization. '
3114                      'This means the contact will see you as offline.'),
3115                    _('_I want this contact to know my status after removal'),
3116                    [DialogButton.make('Cancel'),
3117                     DialogButton.make('Remove',
3118                                       callback=on_ok)],
3119                    modal=False).show()
3120        else:
3121            # several contact to remove at the same time
3122            pritext = _('Remove contacts from contact list')
3123            jids = ''
3124            for contact, _account in list_:
3125                jids += '%(name)s (%(jid)s)\n' % {
3126                    'name': contact.get_shown_name(),
3127                    'jid': contact.jid}
3128            sectext = _('By removing the following contacts, you will also '
3129                        'remove authorization. This means they will see you '
3130                        'as offline:\n\n%s') % jids
3131            ConfirmationDialog(
3132                _('Remove Contacts'),
3133                pritext,
3134                sectext,
3135                [DialogButton.make('Cancel'),
3136                 DialogButton.make('Remove',
3137                                   callback=on_ok2)]).show()
3138
3139    def on_publish_tune_toggled(self, widget, account):
3140        active = widget.get_active()
3141        client = app.get_client(account)
3142        client.get_module('UserTune').set_enabled(active)
3143
3144    def on_publish_location_toggled(self, widget, account):
3145        active = widget.get_active()
3146        client = app.get_client(account)
3147        app.settings.set_account_setting(account, 'publish_location', active)
3148
3149        if active:
3150            location.enable()
3151        else:
3152            client = app.get_client(account)
3153            client.set_user_location(None)
3154
3155        client.get_module('Caps').update_caps()
3156
3157    def on_add_new_contact(self, widget, account):
3158        AddNewContactWindow(account)
3159
3160    def on_create_gc_activate(self, widget, account):
3161        """
3162        When the create gc menuitem is clicked, show the create gc window
3163        """
3164        app.app.activate_action('create-groupchat',
3165                                GLib.Variant('s', account))
3166
3167    def on_show_transports_action(self, action, param):
3168        app.settings.set('show_transports_group', param.get_boolean())
3169        action.set_state(param)
3170        self.refilter_shown_roster_items()
3171
3172    def on_execute_command(self, widget, contact, account, resource=None):
3173        """
3174        Execute command. Full JID needed; if it is other contact, resource is
3175        necessary. Widget is unnecessary, only to be able to make this a
3176        callback
3177        """
3178        jid = contact.jid
3179        if resource:
3180            jid = jid + '/' + resource
3181        AdHocCommand(account, jid)
3182
3183    def on_view_server_info(self, _widget, account):
3184        app.app.activate_action('%s-server-info' % account,
3185                                GLib.Variant('s', account))
3186
3187    def on_roster_window_focus_in_event(self, widget, event):
3188        # roster received focus, so if we had urgency REMOVE IT
3189        # NOTE: we do not have to read the message to remove urgency
3190        # so this functions does that
3191        set_urgency_hint(widget, False)
3192
3193        # if a contact row is selected, update colors (eg. for status msg)
3194        # because gtk engines may differ in bg when window is selected
3195        # or not
3196        if self._last_selected_contact:
3197            for (jid, account) in self._last_selected_contact:
3198                self.draw_contact(jid, account, selected=True, focus=True)
3199
3200    def on_roster_window_focus_out_event(self, widget, event):
3201        # if a contact row is selected, update colors (eg. for status msg)
3202        # because gtk engines may differ in bg when window is selected
3203        # or not
3204        if self._last_selected_contact:
3205            for (jid, account) in self._last_selected_contact:
3206                self.draw_contact(jid, account, selected=True, focus=False)
3207
3208    def on_roster_window_key_press_event(self, widget, event):
3209        if event.keyval == Gdk.KEY_Escape:
3210            if self.rfilter_enabled:
3211                self.disable_rfilter()
3212                return True
3213            if app.interface.msg_win_mgr.mode == \
3214            MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER and \
3215            app.interface.msg_win_mgr.one_window_opened():
3216                # let message window close the tab
3217                return
3218            list_of_paths = self.tree.get_selection().get_selected_rows()[1]
3219            if not list_of_paths and not app.settings.get(
3220            'quit_on_roster_x_button') and ((app.interface.systray_enabled and\
3221            app.settings.get('trayicon') == 'always') or app.settings.get(
3222            'allow_hide_roster')):
3223                if os.name == 'nt' or app.settings.get('hide_on_roster_x_button'):
3224                    self.window.hide()
3225                else:
3226                    self.window.iconify()
3227        elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \
3228        Gdk.KEY_i:
3229            treeselection = self.tree.get_selection()
3230            model, list_of_paths = treeselection.get_selected_rows()
3231            for path in list_of_paths:
3232                type_ = model[path][Column.TYPE]
3233                if type_ in ('contact', 'agent'):
3234                    jid = model[path][Column.JID]
3235                    account = model[path][Column.ACCOUNT]
3236                    contact = app.contacts.get_first_contact_from_jid(account,
3237                        jid)
3238                    self.on_info(widget, contact, account)
3239        elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \
3240        Gdk.KEY_h:
3241            if app.settings.get('one_message_window') == 'always_with_roster':
3242                # Let MessageWindow handle this
3243                return
3244            treeselection = self.tree.get_selection()
3245            model, list_of_paths = treeselection.get_selected_rows()
3246            if len(list_of_paths) != 1:
3247                return
3248            path = list_of_paths[0]
3249            type_ = model[path][Column.TYPE]
3250            if type_ in ('contact', 'agent'):
3251                jid = model[path][Column.JID]
3252                account = model[path][Column.ACCOUNT]
3253                contact = app.contacts.get_first_contact_from_jid(account,
3254                    jid)
3255                dict_ = {'jid': GLib.Variant('s', jid),
3256                         'account': GLib.Variant('s', account)}
3257                app.app.activate_action('browse-history',
3258                                        GLib.Variant('a{sv}', dict_))
3259
3260    def on_roster_window_popup_menu(self, widget):
3261        event = Gdk.Event.new(Gdk.EventType.KEY_PRESS)
3262        self.show_treeview_menu(event)
3263
3264    def on_row_activated(self, widget, path):
3265        """
3266        When an iter is activated (double-click or single click if gnome is set
3267        this way)
3268        """
3269        model = self.modelfilter
3270        account = model[path][Column.ACCOUNT]
3271        type_ = model[path][Column.TYPE]
3272        if type_ in ('group', 'account'):
3273            if self.tree.row_expanded(path):
3274                self.tree.collapse_row(path)
3275            else:
3276                self.tree.expand_row(path, False)
3277            return
3278        if self.rfilter_enabled:
3279            GObject.idle_add(self.disable_rfilter)
3280        jid = model[path][Column.JID]
3281        resource = None
3282        contact = app.contacts.get_contact_with_highest_priority(account, jid)
3283        titer = model.get_iter(path)
3284        if contact.is_groupchat:
3285            first_ev = app.events.get_first_event(account, jid)
3286            if first_ev and self.open_event(account, jid, first_ev):
3287                # We are invited to a GC
3288                # open event cares about connecting to it
3289                self.remove_groupchat(jid, account)
3290            else:
3291                self.on_groupchat_maximized(None, jid, account)
3292            return
3293
3294        # else
3295        first_ev = app.events.get_first_event(account, jid)
3296        if not first_ev:
3297            # look in other resources
3298            for c in app.contacts.get_contacts(account, jid):
3299                fjid = c.get_full_jid()
3300                first_ev = app.events.get_first_event(account, fjid)
3301                if first_ev:
3302                    resource = c.resource
3303                    break
3304        if not first_ev and model.iter_has_child(titer):
3305            child_iter = model.iter_children(titer)
3306            while not first_ev and child_iter:
3307                child_jid = model[child_iter][Column.JID]
3308                first_ev = app.events.get_first_event(account, child_jid)
3309                if first_ev:
3310                    jid = child_jid
3311                else:
3312                    child_iter = model.iter_next(child_iter)
3313        session = None
3314        if first_ev:
3315            if first_ev.type_ in ('chat', 'normal'):
3316                session = first_ev.session
3317            fjid = jid
3318            if resource:
3319                fjid += '/' + resource
3320            if self.open_event(account, fjid, first_ev):
3321                return
3322            # else
3323            contact = app.contacts.get_contact(account, jid, resource)
3324        if not contact or isinstance(contact, list):
3325            contact = app.contacts.get_contact_with_highest_priority(account,
3326                    jid)
3327        if jid == app.get_jid_from_account(account):
3328            resource = None
3329
3330        app.interface.on_open_chat_window(None, contact, account, \
3331            resource=resource, session=session)
3332
3333    def on_roster_treeview_row_activated(self, widget, path, col=0):
3334        """
3335        When an iter is double clicked: open the first event window
3336        """
3337        self.on_row_activated(widget, path)
3338
3339    def on_roster_treeview_row_expanded(self, widget, titer, path):
3340        """
3341        When a row is expanded change the icon of the arrow
3342        """
3343        self._toggeling_row = True
3344        model = widget.get_model()
3345        child_model = model.get_model()
3346        child_iter = model.convert_iter_to_child_iter(titer)
3347
3348        if self.regroup: # merged accounts
3349            accounts = list(app.connections.keys())
3350        else:
3351            accounts = [model[titer][Column.ACCOUNT]]
3352
3353        type_ = model[titer][Column.TYPE]
3354        if type_ == 'group':
3355            group = model[titer][Column.JID]
3356            child_model[child_iter][Column.IMG] = get_icon_name('opened')
3357            if self.rfilter_enabled:
3358                return
3359            for account in accounts:
3360                if group in app.groups[account]: # This account has this group
3361                    app.groups[account][group]['expand'] = True
3362                    if account + group in self.collapsed_rows:
3363                        self.collapsed_rows.remove(account + group)
3364                for contact in app.contacts.iter_contacts(account):
3365                    jid = contact.jid
3366                    if group in contact.groups and \
3367                    app.contacts.is_big_brother(account, jid, accounts) and \
3368                    account + group + jid not in self.collapsed_rows:
3369                        titers = self._get_contact_iter(jid, account)
3370                        for titer_ in titers:
3371                            path = model.get_path(titer_)
3372                            self.tree.expand_row(path, False)
3373        elif type_ == 'account':
3374            account = list(accounts)[0] # There is only one cause we don't use merge
3375            if account in self.collapsed_rows:
3376                self.collapsed_rows.remove(account)
3377            self.draw_account(account)
3378            # When we expand, groups are collapsed. Restore expand state
3379            for group in app.groups[account]:
3380                if app.groups[account][group]['expand']:
3381                    titer = self._get_group_iter(group, account)
3382                    if titer:
3383                        path = model.get_path(titer)
3384                        self.tree.expand_row(path, False)
3385        elif type_ == 'contact':
3386            # Metacontact got toggled, update icon
3387            jid = model[titer][Column.JID]
3388            account = model[titer][Column.ACCOUNT]
3389            contact = app.contacts.get_contact(account, jid)
3390            for group in contact.groups:
3391                if account + group + jid in self.collapsed_rows:
3392                    self.collapsed_rows.remove(account + group + jid)
3393            family = app.contacts.get_metacontacts_family(account, jid)
3394            nearby_family = \
3395                self._get_nearby_family_and_big_brother(family, account)[0]
3396            # Redraw all brothers to show pending events
3397            for data in nearby_family:
3398                self.draw_contact(data['jid'], data['account'])
3399
3400        self._toggeling_row = False
3401
3402    def on_roster_treeview_row_collapsed(self, widget, titer, path):
3403        """
3404        When a row is collapsed change the icon of the arrow
3405        """
3406        self._toggeling_row = True
3407        model = widget.get_model()
3408        child_model = model.get_model()
3409        child_iter = model.convert_iter_to_child_iter(titer)
3410
3411        if self.regroup: # merged accounts
3412            accounts = list(app.connections.keys())
3413        else:
3414            accounts = [model[titer][Column.ACCOUNT]]
3415
3416        type_ = model[titer][Column.TYPE]
3417        if type_ == 'group':
3418            child_model[child_iter][Column.IMG] = get_icon_name('closed')
3419            if self.rfilter_enabled:
3420                return
3421            group = model[titer][Column.JID]
3422            for account in accounts:
3423                if group in app.groups[account]: # This account has this group
3424                    app.groups[account][group]['expand'] = False
3425                    if account + group not in self.collapsed_rows:
3426                        self.collapsed_rows.append(account + group)
3427        elif type_ == 'account':
3428            account = accounts[0] # There is only one cause we don't use merge
3429            if account not in self.collapsed_rows:
3430                self.collapsed_rows.append(account)
3431            self.draw_account(account)
3432        elif type_ == 'contact':
3433            # Metacontact got toggled, update icon
3434            jid = model[titer][Column.JID]
3435            account = model[titer][Column.ACCOUNT]
3436            contact = app.contacts.get_contact(account, jid)
3437            groups = contact.groups
3438            if not groups:
3439                groups = [_('General')]
3440            for group in groups:
3441                if account + group + jid not in self.collapsed_rows:
3442                    self.collapsed_rows.append(account + group + jid)
3443            family = app.contacts.get_metacontacts_family(account, jid)
3444            nearby_family = \
3445                    self._get_nearby_family_and_big_brother(family, account)[0]
3446            # Redraw all brothers to show pending events
3447            for data in nearby_family:
3448                self.draw_contact(data['jid'], data['account'])
3449
3450        self._toggeling_row = False
3451
3452    def on_modelfilter_row_has_child_toggled(self, model, path, titer):
3453        """
3454        Called when a row has gotten the first or lost its last child row
3455
3456        Expand Parent if necessary.
3457        """
3458        if self._toggeling_row:
3459            # Signal is emitted when we write to our model
3460            return
3461
3462        type_ = model[titer][Column.TYPE]
3463        account = model[titer][Column.ACCOUNT]
3464        if not account:
3465            return
3466
3467        if type_ == 'contact':
3468            child_iter = model.convert_iter_to_child_iter(titer)
3469            if self.model.iter_has_child(child_iter):
3470                # we are a bigbrother metacontact
3471                # redraw us to show/hide expand icon
3472                if self.filtering:
3473                    # Prevent endless loops
3474                    jid = model[titer][Column.JID]
3475                    GLib.idle_add(self.draw_contact, jid, account)
3476        elif type_ == 'group':
3477            group = model[titer][Column.JID]
3478            GLib.idle_add(self._adjust_group_expand_collapse_state, group, account)
3479        elif type_ == 'account':
3480            GLib.idle_add(self._adjust_account_expand_collapse_state, account)
3481
3482# Selection can change when the model is filtered
3483# Only write to the model when filtering is finished!
3484#
3485# FIXME: When we are filtering our custom colors are somehow lost
3486#
3487#       def on_treeview_selection_changed(self, selection):
3488#               '''Called when selection in TreeView has changed.
3489#
3490#               Redraw unselected rows to make status message readable
3491#               on all possible backgrounds.
3492#               '''
3493#               model, list_of_paths = selection.get_selected_rows()
3494#               if len(self._last_selected_contact):
3495#                       # update unselected rows
3496#                       for (jid, account) in self._last_selected_contact:
3497#                               GLib.idle_add(self.draw_contact, jid,
3498#                                       account)
3499#               self._last_selected_contact = []
3500#               if len(list_of_paths) == 0:
3501#                       return
3502#               for path in list_of_paths:
3503#                       row = model[path]
3504#                       if row[Column.TYPE] != 'contact':
3505#                               self._last_selected_contact = []
3506#                               return
3507#                       jid = row[Column.JID]
3508#                       account = row[Column.ACCOUNT]
3509#                       self._last_selected_contact.append((jid, account))
3510#                       GLib.idle_add(self.draw_contact, jid, account, True)
3511
3512
3513    def on_service_disco_menuitem_activate(self, widget, account):
3514        server_jid = app.settings.get_account_setting(account, 'hostname')
3515        if server_jid in app.interface.instances[account]['disco']:
3516            app.interface.instances[account]['disco'][server_jid].\
3517                window.present()
3518        else:
3519            try:
3520                # Object will add itself to the window dict
3521                ServiceDiscoveryWindow(account, address_entry=True)
3522            except GajimGeneralException:
3523                pass
3524
3525    def on_show_offline_contacts_action(self, action, param):
3526        """
3527        When show offline option is changed: redraw the treeview
3528        """
3529        action.set_state(param)
3530        app.settings.set('showoffline', param.get_boolean())
3531        self.refilter_shown_roster_items()
3532        self.window.lookup_action('show-active').set_enabled(
3533            not param.get_boolean())
3534
3535    def on_show_active_contacts_action(self, action, param):
3536        """
3537        When show only active contact option is changed: redraw the treeview
3538        """
3539        action.set_state(param)
3540        app.settings.set('show_only_chat_and_online', param.get_boolean())
3541        self.refilter_shown_roster_items()
3542        self.window.lookup_action('show-offline').set_enabled(
3543            not param.get_boolean())
3544
3545    def on_show_roster_action(self, action, param):
3546        # when num controls is 0 this menuitem is hidden, but still need to
3547        # disable keybinding
3548        action.set_state(param)
3549        if self.hpaned.get_child2() is not None:
3550            self.show_roster_vbox(param.get_boolean())
3551
3552    def on_rfilter_entry_changed(self, widget):
3553        """ When we update the content of the filter """
3554        self.rfilter_string = widget.get_text().lower()
3555        if self.rfilter_string == '':
3556            self.disable_rfilter()
3557        self.refilter_shown_roster_items()
3558        # select first row
3559        self.tree.get_selection().unselect_all()
3560        def _func(model, path, iter_, param):
3561            if model[iter_][Column.TYPE] == 'contact' and self.rfilter_string in \
3562            model[iter_][Column.NAME].lower():
3563                col = self.tree.get_column(0)
3564                self.tree.set_cursor_on_cell(path, col, None, False)
3565                return True
3566        self.modelfilter.foreach(_func, None)
3567
3568    def on_rfilter_entry_icon_press(self, widget, icon, event):
3569        """
3570        Disable the roster filtering by clicking the icon in the textEntry
3571        """
3572        self.disable_rfilter()
3573
3574    def on_rfilter_entry_key_press_event(self, widget, event):
3575        if event.keyval == Gdk.KEY_Escape:
3576            self.disable_rfilter()
3577        elif event.keyval == Gdk.KEY_Return:
3578            self.tree.grab_focus()
3579            self.tree.event(event)
3580            self.disable_rfilter()
3581        elif event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down):
3582            self.tree.grab_focus()
3583            self.tree.event(event)
3584        elif event.keyval == Gdk.KEY_BackSpace:
3585            if widget.get_text() == '':
3586                self.disable_rfilter()
3587
3588    def enable_rfilter(self, search_string):
3589        self.rfilter_entry.set_visible(True)
3590        self.rfilter_entry.set_editable(True)
3591        self.rfilter_entry.grab_focus()
3592        if self.rfilter_enabled:
3593            self.rfilter_entry.set_text(self.rfilter_entry.get_text() + \
3594                search_string)
3595        else:
3596            self.rfilter_enabled = True
3597            self.rfilter_entry.set_text(search_string)
3598            self.tree.expand_all()
3599        self.rfilter_entry.set_position(-1)
3600
3601        # If roster is hidden, let's temporarily show it. This can happen if user
3602        # enables rfilter via keyboard shortcut.
3603        self.show_roster_vbox(True)
3604
3605    def disable_rfilter(self):
3606        self.rfilter_enabled = False
3607        self.rfilter_entry.set_text('')
3608        self.rfilter_entry.set_visible(False)
3609        self.rfilter_entry.set_editable(False)
3610        self.refilter_shown_roster_items()
3611        self.tree.grab_focus()
3612        self._readjust_expand_collapse_state()
3613
3614        # If roster was hidden before enable_rfilter was called, hide it back.
3615        state = self.window.lookup_action('show-roster').get_state().get_boolean()
3616        if state is False and self.hpaned.get_child2() is not None:
3617            self.show_roster_vbox(False)
3618
3619    def on_roster_hpaned_notify(self, pane, gparamspec):
3620        """
3621        Keep changing the width of the roster
3622        (when a Gtk.Paned widget handle is dragged)
3623        """
3624        if gparamspec and gparamspec.name == 'position':
3625            roster_width = pane.get_child1().get_allocation().width
3626            app.settings.set('roster_width', roster_width)
3627            app.settings.set('roster_hpaned_position', pane.get_position())
3628
3629################################################################################
3630### Drag and Drop handling
3631################################################################################
3632
3633    def drag_data_get_data(self, treeview, context, selection, target_id,
3634    etime):
3635        model, list_of_paths = self.tree.get_selection().get_selected_rows()
3636        if len(list_of_paths) != 1:
3637            return
3638        path = list_of_paths[0]
3639        data = ''
3640        if path.get_depth() >= 2:
3641            data = model[path][Column.JID]
3642        selection.set_text(data, -1)
3643
3644    def drag_begin(self, treeview, context):
3645        self.dragging = True
3646
3647    def drag_end(self, treeview, context):
3648        self.dragging = False
3649
3650    def on_drop_rosterx(self, widget, account_source, c_source, account_dest,
3651                        c_dest, was_big_brother, context, etime):
3652        type_ = 'message'
3653        if (c_dest.show not in ('offline', 'error') and
3654                c_dest.supports(Namespace.ROSTERX)):
3655            type_ = 'iq'
3656        con = app.connections[account_dest]
3657        con.get_module('RosterItemExchange').send_contacts(
3658            [c_source], c_dest.get_full_jid(), type_=type_)
3659
3660    def on_drop_in_contact(self, widget, account_source, c_source, account_dest,
3661    c_dest, was_big_brother, context, etime):
3662        con_source = app.connections[account_source]
3663        con_dest = app.connections[account_dest]
3664        if (not con_source.get_module('MetaContacts').available or
3665                not con_dest.get_module('MetaContacts').available):
3666            return
3667
3668        def merge_contacts(is_checked=None):
3669            contacts = 0
3670            if is_checked is not None: # dialog has been shown
3671                if is_checked: # user does not want to be asked again
3672                    app.settings.set('confirm_metacontacts', 'no')
3673                else:
3674                    app.settings.set('confirm_metacontacts', 'yes')
3675
3676            # We might have dropped on a metacontact.
3677            # Remove it and add it again later with updated family info
3678            dest_family = app.contacts.get_metacontacts_family(account_dest,
3679                c_dest.jid)
3680            if dest_family:
3681                self._remove_metacontact_family(dest_family, account_dest)
3682                source_family = app.contacts.get_metacontacts_family(
3683                    account_source, c_source.jid)
3684                if dest_family == source_family:
3685                    n = contacts = len(dest_family)
3686                    for tag in source_family:
3687                        if tag['jid'] == c_source.jid:
3688                            tag['order'] = contacts
3689                            continue
3690                        if 'order' in tag:
3691                            n -= 1
3692                            tag['order'] = n
3693            else:
3694                self._remove_entity(c_dest, account_dest)
3695
3696            old_family = app.contacts.get_metacontacts_family(account_source,
3697                    c_source.jid)
3698            old_groups = c_source.groups
3699
3700            # Remove old source contact(s)
3701            if was_big_brother:
3702                # We have got little brothers. Add them all back
3703                self._remove_metacontact_family(old_family, account_source)
3704            else:
3705                # We are only a little brother. Simply remove us from our big
3706                # brother
3707                if self._get_contact_iter(c_source.jid, account_source):
3708                    # When we have been in the group before.
3709                    # Do not try to remove us again
3710                    self._remove_entity(c_source, account_source)
3711
3712                own_data = {}
3713                own_data['jid'] = c_source.jid
3714                own_data['account'] = account_source
3715                # Don't touch the rest of the family
3716                old_family = [own_data]
3717
3718            # Apply new tag and update contact
3719            for data in old_family:
3720                if account_source != data['account'] and not self.regroup:
3721                    continue
3722
3723                _account = data['account']
3724                _jid = data['jid']
3725                _contact = app.contacts.get_first_contact_from_jid(_account,
3726                    _jid)
3727                if not _contact:
3728                    # One of the metacontacts may be not connected.
3729                    continue
3730
3731                _contact.groups = c_dest.groups[:]
3732                app.contacts.add_metacontact(account_dest, c_dest.jid,
3733                    _account, _contact.jid, contacts)
3734                con = app.connections[account_source]
3735                con.get_module('Roster').update_contact(
3736                    _contact.jid, _contact.name, _contact.groups)
3737
3738            # Re-add all and update GUI
3739            new_family = app.contacts.get_metacontacts_family(account_source,
3740                c_source.jid)
3741            brothers = self._add_metacontact_family(new_family, account_source)
3742
3743            for c, acc in brothers:
3744                self.draw_completely(c.jid, acc)
3745
3746            old_groups.extend(c_dest.groups)
3747            for g in old_groups:
3748                self.draw_group(g, account_source)
3749
3750            self.draw_account(account_source)
3751            context.finish(True, True, etime)
3752
3753        dest_family = app.contacts.get_metacontacts_family(account_dest,
3754            c_dest.jid)
3755        source_family = app.contacts.get_metacontacts_family(account_source,
3756            c_source.jid)
3757        confirm_metacontacts = app.settings.get('confirm_metacontacts')
3758        if confirm_metacontacts == 'no' or dest_family == source_family:
3759            merge_contacts()
3760            return
3761        pritext = _('You are about to create a metacontact')
3762        sectext = _('Metacontacts are a way to regroup several contacts in '
3763                    'one single contact. Generally it is used when the same '
3764                    'person has several XMPP- or Transport-Accounts.')
3765        ConfirmationCheckDialog(
3766            _('Create Metacontact'),
3767            pritext,
3768            sectext,
3769            _('_Do not ask me again'),
3770            [DialogButton.make('Cancel'),
3771             DialogButton.make('Accept',
3772                               text=_('_Create'),
3773                               callback=merge_contacts)]).show()
3774
3775    def on_drop_in_group(self, widget, account, c_source, grp_dest,
3776    is_big_brother, context, etime, grp_source=None):
3777        if is_big_brother:
3778            # add whole metacontact to new group
3779            self.add_contact_to_groups(c_source.jid, account, [grp_dest, ])
3780            # remove afterwards so the contact is not moved to General in the
3781            # meantime
3782            if grp_dest != grp_source:
3783                self.remove_contact_from_groups(c_source.jid, account,
3784                    [grp_source])
3785        else:
3786            # Normal contact or little brother
3787            family = app.contacts.get_metacontacts_family(account,
3788                c_source.jid)
3789            if family:
3790                # Little brother
3791                # Remove whole family. Remove us from the family.
3792                # Then re-add other family members.
3793                self._remove_metacontact_family(family, account)
3794                app.contacts.remove_metacontact(account, c_source.jid)
3795                for data in family:
3796                    if account != data['account'] and not self.regroup:
3797                        continue
3798                    if data['jid'] == c_source.jid and\
3799                    data['account'] == account:
3800                        continue
3801                    self.add_contact(data['jid'], data['account'])
3802                    break
3803
3804                self.add_contact_to_groups(c_source.jid, account, [grp_dest, ])
3805
3806            else:
3807                # Normal contact
3808                self.add_contact_to_groups(c_source.jid, account, [grp_dest, ])
3809                # remove afterwards so the contact is not moved to General in
3810                # the meantime
3811                if grp_dest != grp_source:
3812                    self.remove_contact_from_groups(c_source.jid, account,
3813                        [grp_source])
3814
3815        if context.get_actions() in (Gdk.DragAction.MOVE, Gdk.DragAction.COPY):
3816            context.finish(True, True, etime)
3817
3818    def drag_drop(self, treeview, context, x, y, timestamp):
3819        treeview.stop_emission_by_name('drag-drop')
3820        target_list = treeview.drag_dest_get_target_list()
3821        target = treeview.drag_dest_find_target(context, target_list)
3822        treeview.drag_get_data(context, target, timestamp)
3823        return True
3824
3825    def move_group(self, old_name, new_name, account):
3826        for group in list(app.groups[account].keys()):
3827            if group.startswith(old_name):
3828                self.rename_group(group, group.replace(old_name, new_name),
3829                    account)
3830
3831    def drag_data_received_data(self, treeview, context, x, y, selection, info,
3832    etime):
3833        treeview.stop_emission_by_name('drag-data-received')
3834        drop_info = treeview.get_dest_row_at_pos(x, y)
3835        if not drop_info:
3836            return
3837        data = selection.get_data().decode()
3838        if not data:
3839            return # prevents tb when several entries are dragged
3840        model = treeview.get_model()
3841
3842        path_dest, position = drop_info
3843
3844        if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 2 \
3845        and path_dest[1] == 0: # dropped before the first group
3846            return
3847        if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 2:
3848            # dropped before a group: we drop it in the previous group every
3849            # time
3850            path_dest = (path_dest[0], path_dest[1]-1)
3851        # destination: the row something got dropped on
3852        iter_dest = model.get_iter(path_dest)
3853        type_dest = model[iter_dest][Column.TYPE]
3854        jid_dest = model[iter_dest][Column.JID]
3855        account_dest = model[iter_dest][Column.ACCOUNT]
3856
3857        # drop on account row in merged mode, we cannot know the desired account
3858        if account_dest == 'all':
3859            return
3860        # nothing can be done, if destination account is offline
3861        if not app.account_is_available(account_dest):
3862            return
3863
3864        # A file got dropped on the roster
3865        if info == self.TARGET_TYPE_URI_LIST:
3866            if len(path_dest) < 3:
3867                return
3868            if type_dest != 'contact':
3869                return
3870            c_dest = app.contacts.get_contact_with_highest_priority(
3871                account_dest, jid_dest)
3872            if not c_dest.supports(Namespace.JINGLE_FILE_TRANSFER_5):
3873                return
3874            uri = data.strip()
3875            uri_splitted = uri.split() # we may have more than one file dropped
3876            try:
3877                # This is always the last element in windows
3878                uri_splitted.remove('\0')
3879            except ValueError:
3880                pass
3881            nb_uri = len(uri_splitted)
3882            # Check the URIs
3883            bad_uris = []
3884            for a_uri in uri_splitted:
3885                path = helpers.get_file_path_from_dnd_dropped_uri(a_uri)
3886                if not os.path.isfile(path):
3887                    bad_uris.append(a_uri)
3888            if bad_uris:
3889                ErrorDialog(_('Invalid file URI:'), '\n'.join(bad_uris))
3890                return
3891            def _on_send_files(account, jid, uris):
3892                c = app.contacts.get_contact_with_highest_priority(account,
3893                    jid)
3894                for uri in uris:
3895                    path = helpers.get_file_path_from_dnd_dropped_uri(uri)
3896                    if os.path.isfile(path): # is it file?
3897                        app.interface.instances['file_transfers'].send_file(
3898                            account, c, path)
3899            # Popup dialog to confirm sending
3900            text = i18n.ngettext(
3901                'Send this file to %s:\n',
3902                'Send these files to %s:\n',
3903                nb_uri) % c_dest.get_shown_name()
3904
3905            for uri in uri_splitted:
3906                path = helpers.get_file_path_from_dnd_dropped_uri(uri)
3907                text += '\n' + os.path.basename(path)
3908            ConfirmationDialog(
3909                _('File Transfer'),
3910                _('File Transfer'),
3911                text,
3912                [DialogButton.make('Cancel'),
3913                 DialogButton.make('Accept',
3914                                   text=_('_Send'),
3915                                   callback=_on_send_files,
3916                                   args=(account_dest, jid_dest, uri_splitted))],
3917                transient_for=self.window).show()
3918            return
3919
3920        # Check if something is selected
3921        if treeview.get_selection().count_selected_rows() == 0:
3922            return
3923
3924        # a roster entry was dragged and dropped somewhere in the roster
3925
3926        # source: the row that was dragged
3927        path_source = treeview.get_selection().get_selected_rows()[1][0]
3928        iter_source = model.get_iter(path_source)
3929        type_source = model[iter_source][Column.TYPE]
3930        account_source = model[iter_source][Column.ACCOUNT]
3931
3932        if app.settings.get_account_setting(account_source, 'is_zeroconf'):
3933            return
3934
3935        if type_dest == 'self_contact':
3936            # drop on self contact row
3937            return
3938
3939        if type_dest == 'groupchat':
3940            # Drop on a minimized groupchat
3941            if type_source != 'contact':
3942                return
3943            contact_jid = data
3944            gc_control = app.get_groupchat_control(account_dest, jid_dest)
3945            if gc_control is not None:
3946                gc_control.invite(contact_jid)
3947            return
3948
3949        if type_source == 'group':
3950            if account_source != account_dest:
3951                # drop on another account
3952                return
3953            grp_source = model[iter_source][Column.JID]
3954            delimiter = app.connections[account_source].get_module('Delimiter').delimiter
3955            grp_source_list = grp_source.split(delimiter)
3956            new_grp = None
3957            if type_dest == 'account':
3958                new_grp = grp_source_list[-1]
3959            elif type_dest == 'group':
3960                grp_dest = model[iter_dest][Column.JID]
3961                # Don't allow to drop on e.g. Groupchats group
3962                if grp_dest in helpers.special_groups:
3963                    return
3964                grp_dest_list = grp_dest.split(delimiter)
3965                # Do not allow to drop on a subgroup of source group
3966                if grp_source_list[0] != grp_dest_list[0]:
3967                    new_grp = model[iter_dest][Column.JID] + delimiter + \
3968                        grp_source_list[-1]
3969            if new_grp:
3970                self.move_group(grp_source, new_grp, account_source)
3971
3972        # Only normal contacts and group can be dragged
3973        if type_source != 'contact':
3974            return
3975
3976        # A contact was dropped
3977        if app.settings.get_account_setting(account_dest, 'is_zeroconf'):
3978            # drop on zeroconf account, adding not possible
3979            return
3980
3981        if type_dest == 'account' and account_source == account_dest:
3982            # drop on the account it was dragged from
3983            return
3984
3985        # Get valid source group, jid and contact
3986        it = iter_source
3987        while model[it][Column.TYPE] == 'contact':
3988            it = model.iter_parent(it)
3989        grp_source = model[it][Column.JID]
3990        if grp_source in (_('Transports'), _('Group chats')):
3991            # a transport or a minimized groupchat was dragged
3992            # we can add it to other accounts but not move it to another group,
3993            # see below
3994            return
3995        jid_source = data
3996        c_source = app.contacts.get_contact_with_highest_priority(
3997            account_source, jid_source)
3998
3999        # Get destination group
4000        grp_dest = None
4001        if type_dest == 'group':
4002            grp_dest = model[iter_dest][Column.JID]
4003        elif type_dest in ('contact', 'agent'):
4004            it = iter_dest
4005            while model[it][Column.TYPE] != 'group':
4006                it = model.iter_parent(it)
4007            grp_dest = model[it][Column.JID]
4008        if grp_dest in helpers.special_groups:
4009            return
4010
4011        if jid_source == jid_dest:
4012            if grp_source == grp_dest and account_source == account_dest:
4013                # Drop on self
4014                return
4015
4016        # contact drop somewhere in or on a foreign account
4017        if (type_dest == 'account' or not self.regroup) and \
4018        account_source != account_dest:
4019            # add to account in specified group
4020            AddNewContactWindow(account=account_dest, contact_jid=jid_source,
4021                user_nick=c_source.name, group=grp_dest)
4022            return
4023
4024        # we may not add contacts from special_groups
4025        if grp_source in helpers.special_groups:
4026            if grp_source == _('Not in contact list'):
4027                AddNewContactWindow(
4028                    account=account_dest,
4029                    contact_jid=jid_source,
4030                    user_nick=c_source.name,
4031                    group=grp_dest)
4032                return
4033            return
4034
4035        # Is the contact we drag a meta contact?
4036        accounts = account_source
4037        if self.regroup:
4038            accounts = app.contacts.get_accounts() or account_source
4039        is_big_brother = app.contacts.is_big_brother(account_source,
4040            jid_source, accounts)
4041
4042        drop_in_middle_of_meta = False
4043        if type_dest == 'contact':
4044            if position == Gtk.TreeViewDropPosition.BEFORE and len(path_dest) == 4:
4045                drop_in_middle_of_meta = True
4046            if position == Gtk.TreeViewDropPosition.AFTER and (len(path_dest) == 4 or\
4047            self.modelfilter.iter_has_child(iter_dest)):
4048                drop_in_middle_of_meta = True
4049        # Contact drop on group row or between two contacts that are
4050        # not metacontacts
4051        if (type_dest == 'group' or position in (Gtk.TreeViewDropPosition.BEFORE,
4052        Gtk.TreeViewDropPosition.AFTER)) and not drop_in_middle_of_meta:
4053            self.on_drop_in_group(None, account_source, c_source, grp_dest,
4054                is_big_brother, context, etime, grp_source)
4055            return
4056
4057        # Contact drop on another contact, make meta contacts
4058        if position == Gtk.TreeViewDropPosition.INTO_OR_AFTER or \
4059        position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE or drop_in_middle_of_meta:
4060            c_dest = app.contacts.get_contact_with_highest_priority(
4061                account_dest, jid_dest)
4062            if not c_dest:
4063                # c_dest is None if jid_dest doesn't belong to account
4064                return
4065            menu = Gtk.Menu()
4066            #from and to are the names of contacts
4067            item = Gtk.MenuItem.new_with_label(_('Send %(from)s to %(to)s') % {
4068                'from': c_source.get_shown_name(), 'to': c_dest.get_shown_name()})
4069            item.set_use_underline(False)
4070            item.connect('activate', self.on_drop_rosterx, account_source,
4071            c_source, account_dest, c_dest, is_big_brother, context, etime)
4072            menu.append(item)
4073
4074            dest_family = app.contacts.get_metacontacts_family(account_dest,
4075                c_dest.jid)
4076            source_family = app.contacts.get_metacontacts_family(
4077                account_source, c_source.jid)
4078            if dest_family == source_family  and dest_family:
4079                item = Gtk.MenuItem.new_with_label(
4080                    _('Make %s first contact') % (
4081                    c_source.get_shown_name()))
4082                item.set_use_underline(False)
4083            else:
4084                item = Gtk.MenuItem.new_with_label(
4085                    _('Make %(contact1)s and %(contact2)s metacontacts') % {
4086                    'contact1': c_source.get_shown_name(), 'contact2': c_dest.get_shown_name()})
4087                item.set_use_underline(False)
4088
4089            item.connect('activate', self.on_drop_in_contact, account_source,
4090            c_source, account_dest, c_dest, is_big_brother, context, etime)
4091
4092            menu.append(item)
4093
4094            menu.attach_to_widget(self.tree, None)
4095            menu.connect('selection-done', gtkgui_helpers.destroy_widget)
4096            menu.show_all()
4097            menu.popup_at_pointer(None)
4098
4099################################################################################
4100### Everything about images and icons....
4101### Cleanup assigned to Jim++ :-)
4102################################################################################
4103
4104    def update_icons(self):
4105        # Update the roster
4106        self.setup_and_draw_roster()
4107
4108        # Update the systray
4109        if app.interface.systray_enabled:
4110            app.interface.systray.set_img()
4111            app.interface.systray.change_status(helpers.get_global_show())
4112
4113        for win in app.interface.msg_win_mgr.windows():
4114            for ctrl in win.controls():
4115                ctrl.update_ui()
4116                win.redraw_tab(ctrl)
4117
4118        self._status_selector.update()
4119
4120
4121    def set_account_status_icon(self, account):
4122        child_iterA = self._get_account_iter(account, self.model)
4123        if not child_iterA:
4124            return
4125        if not self.regroup:
4126            status = helpers.get_connection_status(account)
4127        else: # accounts merged
4128            status = helpers.get_global_show()
4129        self.model[child_iterA][Column.IMG] = get_icon_name(status)
4130
4131################################################################################
4132### Style and theme related methods
4133################################################################################
4134
4135    def show_title(self):
4136        change_title_allowed = app.settings.get('change_roster_title')
4137        if not change_title_allowed:
4138            return
4139
4140        nb_unread = 0
4141        for account in app.connections:
4142            # Count events in roster title only if we don't auto open them
4143            if not helpers.allow_popup_window(account):
4144                nb_unread += app.events.get_nb_events(['chat', 'normal',
4145                    'file-request', 'file-error', 'file-completed',
4146                    'file-request-error', 'file-send-error', 'file-stopped',
4147                    'printed_chat'], account)
4148
4149
4150        if app.settings.get('one_message_window') == 'always_with_roster':
4151            # always_with_roster mode defers to the MessageWindow
4152            if not app.interface.msg_win_mgr.one_window_opened():
4153                # No MessageWindow to defer to
4154                self.window.set_title('Gajim')
4155            set_urgency_hint(self.window, nb_unread > 0)
4156            return
4157
4158        start = ''
4159        if nb_unread > 1:
4160            start = '[' + str(nb_unread) + ']  '
4161        elif nb_unread == 1:
4162            start = '*  '
4163
4164        self.window.set_title(start + 'Gajim')
4165        set_urgency_hint(self.window, nb_unread > 0)
4166
4167    def _nec_chatstate_received(self, event):
4168        if event.contact.is_gc_contact or event.contact.is_pm_contact:
4169            return
4170        self.draw_contact(event.contact.jid, event.account)
4171
4172    def _style_changed(self, *args):
4173        self.change_roster_style(None)
4174
4175    def _change_style(self, model, path, titer, option):
4176        if option is None or model[titer][Column.TYPE] == option:
4177            # We changed style for this type of row
4178            model[titer][Column.NAME] = model[titer][Column.NAME]
4179
4180    def change_roster_style(self, option):
4181        self.model.foreach(self._change_style, option)
4182        for win in app.interface.msg_win_mgr.windows():
4183            win.repaint_themed_widgets()
4184
4185    def repaint_themed_widgets(self):
4186        """
4187        Notify windows that contain themed widgets to repaint them
4188        """
4189        for win in app.interface.msg_win_mgr.windows():
4190            win.repaint_themed_widgets()
4191        for account in app.connections:
4192            for ctrl in list(app.interface.minimized_controls[account].values()):
4193                ctrl.repaint_themed_widgets()
4194
4195    def _iconCellDataFunc(self, column, renderer, model, titer, data=None):
4196        """
4197        When a row is added, set properties for icon renderer
4198        """
4199        icon_name = model[titer][Column.IMG]
4200        if ':' in icon_name:
4201            icon_name, expanded = icon_name.split(':')
4202            surface = get_metacontact_surface(
4203                icon_name, expanded == 'opened', self.scale_factor)
4204            renderer.set_property('icon_name', None)
4205            renderer.set_property('surface', surface)
4206        else:
4207            renderer.set_property('surface', None)
4208            renderer.set_property('icon_name', icon_name)
4209
4210        try:
4211            type_ = model[titer][Column.TYPE]
4212        except TypeError:
4213            return
4214        if type_ == 'account':
4215            self._set_account_row_background_color(renderer)
4216            renderer.set_property('xalign', 0)
4217        elif type_ == 'group':
4218            self._set_group_row_background_color(renderer)
4219            parent_iter = model.iter_parent(titer)
4220            if model[parent_iter][Column.TYPE] == 'group':
4221                renderer.set_property('xalign', 0.4)
4222            else:
4223                renderer.set_property('xalign', 0.6)
4224        elif type_:
4225            # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4226            if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
4227                # This can append when at the moment we add the row
4228                return
4229            jid = model[titer][Column.JID]
4230            account = model[titer][Column.ACCOUNT]
4231            self._set_contact_row_background_color(renderer, jid, account)
4232            parent_iter = model.iter_parent(titer)
4233            if model[parent_iter][Column.TYPE] == 'contact':
4234                renderer.set_property('xalign', 1)
4235            else:
4236                renderer.set_property('xalign', 0.6)
4237        renderer.set_property('width', 26)
4238
4239    def _nameCellDataFunc(self, column, renderer, model, titer, data=None):
4240        """
4241        When a row is added, set properties for name renderer
4242        """
4243        try:
4244            type_ = model[titer][Column.TYPE]
4245        except TypeError:
4246            return
4247
4248        if type_ == 'account':
4249            color = app.css_config.get_value('.gajim-account-row', StyleAttr.COLOR)
4250            renderer.set_property('foreground', color)
4251            desc = app.css_config.get_font('.gajim-account-row')
4252            renderer.set_property('font-desc', desc)
4253            renderer.set_property('xpad', 0)
4254            renderer.set_property('width', 3)
4255            self._set_account_row_background_color(renderer)
4256        elif type_ == 'group':
4257            color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR)
4258            renderer.set_property('foreground', color)
4259            desc = app.css_config.get_font('.gajim-group-row')
4260            renderer.set_property('font-desc', desc)
4261            parent_iter = model.iter_parent(titer)
4262            if model[parent_iter][Column.TYPE] == 'group':
4263                renderer.set_property('xpad', 8)
4264            else:
4265                renderer.set_property('xpad', 4)
4266            self._set_group_row_background_color(renderer)
4267        elif type_:
4268            # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4269            if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
4270                # This can append when at the moment we add the row
4271                return
4272            jid = model[titer][Column.JID]
4273            account = model[titer][Column.ACCOUNT]
4274
4275            color = None
4276            if type_ == 'groupchat':
4277                ctrl = app.interface.minimized_controls[account].get(jid, None)
4278                if ctrl and ctrl.attention_flag:
4279                    color = app.css_config.get_value(
4280                        '.state_muc_directed_msg_color', StyleAttr.COLOR)
4281            elif app.settings.get('show_chatstate_in_roster'):
4282                chatstate = app.contacts.get_combined_chatstate(account, jid)
4283                if chatstate not in (None, 'active'):
4284                    color = app.css_config.get_value(
4285                        '.gajim-state-%s' % chatstate, StyleAttr.COLOR)
4286            else:
4287                color = app.css_config.get_value(
4288                    '.gajim-contact-row', StyleAttr.COLOR)
4289            renderer.set_property('foreground', color)
4290
4291            self._set_contact_row_background_color(renderer, jid, account)
4292            desc = app.css_config.get_font('.gajim-contact-row')
4293            renderer.set_property('font-desc', desc)
4294            parent_iter = model.iter_parent(titer)
4295            if model[parent_iter][Column.TYPE] == 'contact':
4296                renderer.set_property('xpad', 16)
4297            else:
4298                renderer.set_property('xpad', 12)
4299
4300    def _fill_pep_pixbuf_renderer(self, column, renderer, model, titer,
4301    data=None):
4302        """
4303        When a row is added, draw the respective pep icon
4304        """
4305        try:
4306            type_ = model[titer][Column.TYPE]
4307        except TypeError:
4308            return
4309
4310        # allocate space for the icon only if needed
4311        if model[titer][data] is None:
4312            renderer.set_property('visible', False)
4313        else:
4314            renderer.set_property('visible', True)
4315
4316            if type_ == 'account':
4317                self._set_account_row_background_color(renderer)
4318                renderer.set_property('xalign', 1)
4319            elif type_:
4320                if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
4321                    # This can append at the moment we add the row
4322                    return
4323                jid = model[titer][Column.JID]
4324                account = model[titer][Column.ACCOUNT]
4325                self._set_contact_row_background_color(renderer, jid, account)
4326
4327    def _fill_avatar_pixbuf_renderer(self, column, renderer, model, titer,
4328    data=None):
4329        """
4330        When a row is added, set properties for avatar renderer
4331        """
4332        try:
4333            type_ = model[titer][Column.TYPE]
4334        except TypeError:
4335            return
4336
4337        if type_ in ('group', 'account'):
4338            renderer.set_property('visible', False)
4339            return
4340
4341        image = model[titer][Column.AVATAR_IMG]
4342        if image is not None:
4343            surface = image.get_property('surface')
4344            renderer.set_property('surface', surface)
4345        # allocate space for the icon only if needed
4346        if model[titer][Column.AVATAR_IMG] or \
4347        app.settings.get('avatar_position_in_roster') == 'left':
4348            renderer.set_property('visible', True)
4349            if type_:
4350                # prevent type_ = None, see http://trac.gajim.org/ticket/2534
4351                if not model[titer][Column.JID] or not model[titer][Column.ACCOUNT]:
4352                    # This can append at the moment we add the row
4353                    return
4354                jid = model[titer][Column.JID]
4355                account = model[titer][Column.ACCOUNT]
4356                self._set_contact_row_background_color(renderer, jid, account)
4357        else:
4358            renderer.set_property('visible', False)
4359        if model[titer][Column.AVATAR_IMG] is None and \
4360        app.settings.get('avatar_position_in_roster') != 'left':
4361            renderer.set_property('visible', False)
4362
4363        renderer.set_property('width', AvatarSize.ROSTER)
4364        renderer.set_property('xalign', 0.5)
4365
4366    def _fill_padlock_pixbuf_renderer(self, column, renderer, model, titer,
4367    data=None):
4368        """
4369        When a row is added, set properties for padlock renderer
4370        """
4371        try:
4372            type_ = model[titer][Column.TYPE]
4373        except TypeError:
4374            return
4375
4376        # allocate space for the icon only if needed
4377        if type_ == 'account' and model[titer][Column.PADLOCK_PIXBUF]:
4378            renderer.set_property('visible', True)
4379            self._set_account_row_background_color(renderer)
4380            renderer.set_property('xalign', 1) # align pixbuf to the right
4381        else:
4382            renderer.set_property('visible', False)
4383
4384    def _set_account_row_background_color(self, renderer):
4385        color = app.css_config.get_value('.gajim-account-row', StyleAttr.BACKGROUND)
4386        renderer.set_property('cell-background', color)
4387
4388    def _set_contact_row_background_color(self, renderer, jid, account):
4389        if jid in app.newly_added[account]:
4390            renderer.set_property('cell-background', app.css_config.get_value(
4391                    '.gajim-roster-connected', StyleAttr.BACKGROUND))
4392        elif jid in app.to_be_removed[account]:
4393            renderer.set_property('cell-background', app.css_config.get_value(
4394                '.gajim-roster-disconnected', StyleAttr.BACKGROUND))
4395        else:
4396            color = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND)
4397            renderer.set_property('cell-background', color)
4398
4399    def _set_group_row_background_color(self, renderer):
4400        color = app.css_config.get_value('.gajim-group-row', 'background')
4401        renderer.set_property('cell-background', color)
4402
4403################################################################################
4404### Everything about building menus
4405### FIXME: We really need to make it simpler! 1465 lines are a few to much....
4406################################################################################
4407
4408    def build_account_menu(self, account):
4409        # we have to create our own set of icons for the menu
4410        # using self.jabber_status_images is poopoo
4411        if not app.settings.get_account_setting(account, 'is_zeroconf'):
4412            xml = get_builder('account_context_menu.ui')
4413            account_context_menu = xml.get_object('account_context_menu')
4414
4415            status_menuitem = xml.get_object('status_menuitem')
4416            add_contact_menuitem = xml.get_object('add_contact_menuitem')
4417            service_discovery_menuitem = xml.get_object(
4418                'service_discovery_menuitem')
4419            execute_command_menuitem = xml.get_object(
4420                'execute_command_menuitem')
4421            view_server_info_menuitem = xml.get_object(
4422                'view_server_info_menuitem')
4423            edit_account_menuitem = xml.get_object('edit_account_menuitem')
4424            sub_menu = Gtk.Menu()
4425            status_menuitem.set_submenu(sub_menu)
4426
4427            for show in ('online', 'away', 'xa', 'dnd'):
4428                uf_show = helpers.get_uf_show(show, use_mnemonic=True)
4429                item = Gtk.MenuItem.new_with_mnemonic(uf_show)
4430                sub_menu.append(item)
4431                item.connect('activate', self.change_status, account, show)
4432
4433            item = Gtk.SeparatorMenuItem.new()
4434            sub_menu.append(item)
4435
4436            item = Gtk.MenuItem.new_with_mnemonic(_('_Change Status Message'))
4437            sub_menu.append(item)
4438            item.connect('activate', self.on_change_status_message_activate,
4439                account)
4440            if not app.account_is_available(account):
4441                item.set_sensitive(False)
4442
4443            item = Gtk.SeparatorMenuItem.new()
4444            sub_menu.append(item)
4445
4446            uf_show = helpers.get_uf_show('offline', use_mnemonic=True)
4447            item = Gtk.MenuItem.new_with_mnemonic(uf_show)
4448            sub_menu.append(item)
4449            item.connect('activate', self.change_status, account, 'offline')
4450
4451            pep_menuitem = xml.get_object('pep_menuitem')
4452            if app.connections[account].get_module('PEP').supported:
4453                pep_submenu = Gtk.Menu()
4454                pep_menuitem.set_submenu(pep_submenu)
4455
4456                item = Gtk.CheckMenuItem(label=_('Publish Tune'))
4457                pep_submenu.append(item)
4458                if sys.platform in ('win32', 'darwin'):
4459                    item.set_sensitive(False)
4460                else:
4461                    active = app.settings.get_account_setting(account,
4462                                                              'publish_tune')
4463                    item.set_active(active)
4464                    item.connect('toggled', self.on_publish_tune_toggled,
4465                                 account)
4466
4467                item = Gtk.CheckMenuItem(label=_('Publish Location'))
4468                pep_submenu.append(item)
4469                if not app.is_installed('GEOCLUE'):
4470                    item.set_sensitive(False)
4471                else:
4472                    active = app.settings.get_account_setting(
4473                        account, 'publish_location')
4474                    item.set_active(active)
4475                    item.connect('toggled', self.on_publish_location_toggled,
4476                                 account)
4477
4478            else:
4479                pep_menuitem.set_sensitive(False)
4480
4481            edit_account_menuitem.set_detailed_action_name(
4482                'app.accounts::%s' % account)
4483            if app.connections[account].roster_supported:
4484                add_contact_menuitem.connect('activate',
4485                    self.on_add_new_contact, account)
4486            else:
4487                add_contact_menuitem.set_sensitive(False)
4488            service_discovery_menuitem.connect('activate',
4489                self.on_service_disco_menuitem_activate, account)
4490            hostname = app.settings.get_account_setting(account, 'hostname')
4491            contact = app.contacts.create_contact(jid=hostname,
4492                account=account) # Fake contact
4493            execute_command_menuitem.connect('activate',
4494                self.on_execute_command, contact, account)
4495            view_server_info_menuitem.connect('activate',
4496                self.on_view_server_info, account)
4497
4498            # make some items insensitive if account is offline
4499            if not app.account_is_available(account):
4500                for widget in (add_contact_menuitem, service_discovery_menuitem,
4501                execute_command_menuitem, view_server_info_menuitem,
4502                pep_menuitem):
4503                    widget.set_sensitive(False)
4504        else:
4505            xml = get_builder('zeroconf_context_menu.ui')
4506            account_context_menu = xml.get_object('zeroconf_context_menu')
4507
4508            status_menuitem = xml.get_object('status_menuitem')
4509            zeroconf_properties_menuitem = xml.get_object(
4510                    'zeroconf_properties_menuitem')
4511            sub_menu = Gtk.Menu()
4512            status_menuitem.set_submenu(sub_menu)
4513
4514            for show in ('online', 'away', 'dnd'):
4515                uf_show = helpers.get_uf_show(show, use_mnemonic=True)
4516                item = Gtk.MenuItem.new_with_mnemonic(uf_show)
4517                sub_menu.append(item)
4518                item.connect('activate', self.change_status, account, show)
4519
4520            item = Gtk.SeparatorMenuItem.new()
4521            sub_menu.append(item)
4522
4523            item = Gtk.MenuItem.new_with_mnemonic(_('_Change Status Message'))
4524            sub_menu.append(item)
4525            item.connect('activate', self.on_change_status_message_activate,
4526                account)
4527            if not app.account_is_available(account):
4528                item.set_sensitive(False)
4529
4530            uf_show = helpers.get_uf_show('offline', use_mnemonic=True)
4531            item = Gtk.MenuItem.new_with_mnemonic(uf_show)
4532            sub_menu.append(item)
4533            item.connect('activate', self.change_status, account, 'offline')
4534
4535            zeroconf_properties_menuitem.set_detailed_action_name(
4536                'app.accounts::%s' % account)
4537
4538        return account_context_menu
4539
4540    def make_account_menu(self, event, titer):
4541        """
4542        Make account's popup menu
4543        """
4544        model = self.modelfilter
4545        account = model[titer][Column.ACCOUNT]
4546
4547        if account != 'all': # not in merged mode
4548            menu = self.build_account_menu(account)
4549        else:
4550            menu = Gtk.Menu()
4551            accounts = [] # Put accounts in a list to sort them
4552            for account in app.connections:
4553                accounts.append(account)
4554            accounts.sort()
4555            for account in accounts:
4556                label = app.get_account_label(account)
4557                item = Gtk.MenuItem.new_with_label(label)
4558                account_menu = self.build_account_menu(account)
4559                item.set_submenu(account_menu)
4560                menu.append(item)
4561
4562        event_button = gtkgui_helpers.get_possible_button_event(event)
4563
4564        menu.attach_to_widget(self.tree, None)
4565        menu.connect('selection-done', gtkgui_helpers.destroy_widget)
4566        menu.show_all()
4567        menu.popup(None, None, None, None, event_button, event.time)
4568
4569    def make_group_menu(self, event, iters):
4570        """
4571        Make group's popup menu
4572        """
4573        model = self.modelfilter
4574        groups = []
4575        accounts = []
4576
4577        list_ = []  # list of (contact, account) tuples
4578        list_online = []  # list of (contact, account) tuples
4579
4580        for titer in iters:
4581            groups.append(model[titer][Column.JID])
4582            accounts.append(model[titer][Column.ACCOUNT])
4583            # Don't show menu if groups of more than one account are selected
4584            if accounts[0] != model[titer][Column.ACCOUNT]:
4585                return
4586        account = accounts[0]
4587
4588        show_bookmarked = True
4589        for jid in app.contacts.get_jid_list(account):
4590            contact = app.contacts.get_contact_with_highest_priority(account,
4591                jid)
4592            for group in groups:
4593                if group in contact.get_shown_groups():
4594                    if contact.show not in ('offline', 'error'):
4595                        list_online.append((contact, account))
4596                        # Check that all contacts support direct NUC invite
4597                        if not contact.supports(Namespace.CONFERENCE):
4598                            show_bookmarked = False
4599                    list_.append((contact, account))
4600        menu = Gtk.Menu()
4601
4602        # Make special context menu if group is Groupchats
4603        if _('Group chats') in groups:
4604            if len(groups) == 1:
4605                maximize_menuitem = Gtk.MenuItem.new_with_mnemonic(
4606                    _('_Maximize All'))
4607                maximize_menuitem.connect('activate',
4608                    self.on_all_groupchat_maximized, list_)
4609                menu.append(maximize_menuitem)
4610            else:
4611                return
4612        else:
4613            # Send Group Message
4614            send_group_message_item = Gtk.MenuItem.new_with_mnemonic(
4615                _('Send Group M_essage'))
4616
4617            send_group_message_submenu = Gtk.Menu()
4618            send_group_message_item.set_submenu(send_group_message_submenu)
4619            menu.append(send_group_message_item)
4620
4621            group_message_to_all_item = Gtk.MenuItem.new_with_label(_(
4622                'To all users'))
4623            send_group_message_submenu.append(group_message_to_all_item)
4624
4625            group_message_to_all_online_item = Gtk.MenuItem.new_with_label(
4626                _('To all online users'))
4627            send_group_message_submenu.append(group_message_to_all_online_item)
4628
4629            group_message_to_all_online_item.connect('activate',
4630                self.on_send_single_message_menuitem_activate, account,
4631                list_online)
4632            group_message_to_all_item.connect('activate',
4633                self.on_send_single_message_menuitem_activate, account, list_)
4634
4635            # Invite to
4636            invite_menuitem = Gtk.MenuItem.new_with_mnemonic(
4637                _('In_vite to'))
4638            if _('Transports') not in groups:
4639                gui_menu_builder.build_invite_submenu(invite_menuitem,
4640                    list_online, show_bookmarked=show_bookmarked)
4641                menu.append(invite_menuitem)
4642
4643            # there is no singlemessage and custom status for zeroconf
4644            if app.settings.get_account_setting(account, 'is_zeroconf'):
4645                send_group_message_item.set_sensitive(False)
4646
4647            if not app.account_is_available(account):
4648                send_group_message_item.set_sensitive(False)
4649                invite_menuitem.set_sensitive(False)
4650
4651        special_group = False
4652        for group in groups:
4653            if group in helpers.special_groups:
4654                special_group = True
4655                break
4656
4657        if not special_group and len(groups) == 1:
4658            group = groups[0]
4659            item = Gtk.SeparatorMenuItem.new() # separator
4660            menu.append(item)
4661
4662            # Rename
4663            rename_item = Gtk.MenuItem.new_with_mnemonic(_('_Rename…'))
4664            menu.append(rename_item)
4665            rename_item.connect('activate', self.on_rename, 'group', group,
4666                account)
4667
4668            # Remove group
4669            remove_item = Gtk.MenuItem.new_with_mnemonic(_('Remo_ve'))
4670            menu.append(remove_item)
4671            remove_item.connect('activate', self.on_remove_group_item_activated,
4672                group, account)
4673
4674            # unsensitive if account is not connected
4675            if not app.account_is_available(account):
4676                rename_item.set_sensitive(False)
4677
4678            # General group cannot be changed
4679            if group == _('General'):
4680                rename_item.set_sensitive(False)
4681                remove_item.set_sensitive(False)
4682
4683        event_button = gtkgui_helpers.get_possible_button_event(event)
4684
4685        menu.attach_to_widget(self.tree, None)
4686        menu.connect('selection-done', gtkgui_helpers.destroy_widget)
4687        menu.show_all()
4688        menu.popup(None, None, None, None, event_button, event.time)
4689
4690    def make_contact_menu(self, event, titer):
4691        """
4692        Make contact's popup menu
4693        """
4694        model = self.modelfilter
4695        jid = model[titer][Column.JID]
4696        account = model[titer][Column.ACCOUNT]
4697        contact = app.contacts.get_contact_with_highest_priority(account, jid)
4698        menu = gui_menu_builder.get_contact_menu(contact, account)
4699        event_button = gtkgui_helpers.get_possible_button_event(event)
4700        menu.attach_to_widget(self.tree, None)
4701        menu.popup(None, None, None, None, event_button, event.time)
4702
4703    def make_multiple_contact_menu(self, event, iters):
4704        """
4705        Make group's popup menu
4706        """
4707        model = self.modelfilter
4708        list_ = [] # list of (jid, account) tuples
4709        one_account_offline = False
4710        is_blocked = True
4711        blocking_supported = True
4712        for titer in iters:
4713            jid = model[titer][Column.JID]
4714            account = model[titer][Column.ACCOUNT]
4715            if not app.account_is_available(account):
4716                one_account_offline = True
4717
4718            con = app.connections[account]
4719            if not con.get_module('Blocking').supported:
4720                blocking_supported = False
4721            contact = app.contacts.get_contact_with_highest_priority(
4722                account, jid)
4723            if not helpers.jid_is_blocked(account, jid):
4724                is_blocked = False
4725            list_.append((contact, account))
4726
4727        menu = Gtk.Menu()
4728        account = None
4729        for (contact, current_account) in list_:
4730            # check that we use the same account for every sender
4731            if account is not None and account != current_account:
4732                account = None
4733                break
4734            account = current_account
4735        show_bookmarked = True
4736        for (contact, current_account) in list_:
4737            # Check that all contacts support direct NUC invite
4738            if not contact.supports(Namespace.CONFERENCE):
4739                show_bookmarked = False
4740                break
4741        if account is not None:
4742            send_group_message_item = Gtk.MenuItem.new_with_mnemonic(
4743                _('Send Group M_essage'))
4744            menu.append(send_group_message_item)
4745            send_group_message_item.connect('activate',
4746                self.on_send_single_message_menuitem_activate, account, list_)
4747
4748        # Invite to Groupchat
4749        invite_item = Gtk.MenuItem.new_with_mnemonic(_('In_vite to'))
4750
4751        gui_menu_builder.build_invite_submenu(invite_item, list_,
4752            show_bookmarked=show_bookmarked)
4753        menu.append(invite_item)
4754
4755        item = Gtk.SeparatorMenuItem.new() # separator
4756        menu.append(item)
4757
4758        # Manage Transport submenu
4759        item = Gtk.MenuItem.new_with_mnemonic(_('_Manage Contacts'))
4760        manage_contacts_submenu = Gtk.Menu()
4761        item.set_submenu(manage_contacts_submenu)
4762        menu.append(item)
4763
4764        # Edit Groups
4765        edit_groups_item = Gtk.MenuItem.new_with_mnemonic(_('Edit _Groups…'))
4766        manage_contacts_submenu.append(edit_groups_item)
4767        edit_groups_item.connect('activate', self.on_edit_groups, list_)
4768
4769        item = Gtk.SeparatorMenuItem.new() # separator
4770        manage_contacts_submenu.append(item)
4771
4772        # Block
4773        if is_blocked and blocking_supported:
4774            unblock_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Unblock'))
4775            unblock_menuitem.connect('activate', self.on_unblock, list_)
4776            manage_contacts_submenu.append(unblock_menuitem)
4777        else:
4778            block_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Block'))
4779            block_menuitem.connect('activate', self.on_block, list_)
4780            manage_contacts_submenu.append(block_menuitem)
4781
4782            if not blocking_supported:
4783                block_menuitem.set_sensitive(False)
4784
4785        # Remove
4786        remove_item = Gtk.MenuItem.new_with_mnemonic(_('_Remove'))
4787        manage_contacts_submenu.append(remove_item)
4788        remove_item.connect('activate', self.on_req_usub, list_)
4789        # unsensitive remove if one account is not connected
4790        if one_account_offline:
4791            remove_item.set_sensitive(False)
4792
4793        event_button = gtkgui_helpers.get_possible_button_event(event)
4794
4795        menu.attach_to_widget(self.tree, None)
4796        menu.connect('selection-done', gtkgui_helpers.destroy_widget)
4797        menu.show_all()
4798        menu.popup(None, None, None, None, event_button, event.time)
4799
4800    def make_transport_menu(self, event, titer):
4801        """
4802        Make transport's popup menu
4803        """
4804        model = self.modelfilter
4805        jid = model[titer][Column.JID]
4806        account = model[titer][Column.ACCOUNT]
4807        contact = app.contacts.get_contact_with_highest_priority(account, jid)
4808        menu = gui_menu_builder.get_transport_menu(contact, account)
4809        event_button = gtkgui_helpers.get_possible_button_event(event)
4810        menu.attach_to_widget(self.tree, None)
4811        menu.popup(None, None, None, None, event_button, event.time)
4812
4813    def make_groupchat_menu(self, event, titer):
4814        model = self.modelfilter
4815
4816        jid = model[titer][Column.JID]
4817        account = model[titer][Column.ACCOUNT]
4818        contact = app.contacts.get_contact_with_highest_priority(account, jid)
4819        menu = Gtk.Menu()
4820
4821        if jid in app.interface.minimized_controls[account]:
4822            maximize_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
4823                '_Maximize'))
4824            maximize_menuitem.connect('activate', self.on_groupchat_maximized, \
4825                jid, account)
4826            menu.append(maximize_menuitem)
4827
4828            rename_menuitem = Gtk.MenuItem.new_with_mnemonic(_('Re_name'))
4829            rename_menuitem.connect('activate',
4830                                    self.on_groupchat_rename,
4831                                    jid,
4832                                    account)
4833            menu.append(rename_menuitem)
4834
4835        disconnect_menuitem = Gtk.MenuItem.new_with_mnemonic(_(
4836            '_Leave'))
4837        disconnect_menuitem.connect('activate', self.on_disconnect, jid,
4838            account)
4839        menu.append(disconnect_menuitem)
4840
4841        item = Gtk.SeparatorMenuItem.new() # separator
4842        menu.append(item)
4843
4844        adhoc_menuitem = Gtk.MenuItem.new_with_mnemonic(_('Execute command'))
4845        adhoc_menuitem.connect('activate', self.on_execute_command, contact,
4846            account)
4847        menu.append(adhoc_menuitem)
4848
4849        item = Gtk.SeparatorMenuItem.new() # separator
4850        menu.append(item)
4851
4852        history_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_History'))
4853        history_menuitem.set_action_name('app.browse-history')
4854        dict_ = {'jid': GLib.Variant('s', contact.jid),
4855                 'account': GLib.Variant('s', account)}
4856        variant = GLib.Variant('a{sv}', dict_)
4857        history_menuitem.set_action_target_value(variant)
4858
4859        menu.append(history_menuitem)
4860
4861        event_button = gtkgui_helpers.get_possible_button_event(event)
4862
4863        menu.attach_to_widget(self.tree, None)
4864        menu.connect('selection-done', gtkgui_helpers.destroy_widget)
4865        menu.show_all()
4866        menu.popup(None, None, None, None, event_button, event.time)
4867
4868    def show_appropriate_context_menu(self, event, iters):
4869        # iters must be all of the same type
4870        model = self.modelfilter
4871        type_ = model[iters[0]][Column.TYPE]
4872        for titer in iters[1:]:
4873            if model[titer][Column.TYPE] != type_:
4874                return
4875        if type_ == 'group':
4876            self.make_group_menu(event, iters)
4877        if type_ == 'groupchat' and len(iters) == 1:
4878            self.make_groupchat_menu(event, iters[0])
4879        elif type_ == 'agent' and len(iters) == 1:
4880            self.make_transport_menu(event, iters[0])
4881        elif type_ in ('contact', 'self_contact') and len(iters) == 1:
4882            self.make_contact_menu(event, iters[0])
4883        elif type_ == 'contact':
4884            self.make_multiple_contact_menu(event, iters)
4885        elif type_ == 'account' and len(iters) == 1:
4886            self.make_account_menu(event, iters[0])
4887
4888    def show_treeview_menu(self, event):
4889        try:
4890            model, list_of_paths = self.tree.get_selection().get_selected_rows()
4891        except TypeError:
4892            self.tree.get_selection().unselect_all()
4893            return
4894        if not list_of_paths:
4895            # no row is selected
4896            return
4897        if len(list_of_paths) > 1:
4898            iters = []
4899            for path in list_of_paths:
4900                iters.append(model.get_iter(path))
4901        else:
4902            path = list_of_paths[0]
4903            iters = [model.get_iter(path)]
4904        self.show_appropriate_context_menu(event, iters)
4905
4906        return True
4907
4908    def fill_column(self, col):
4909        for rend in self.renderers_list:
4910            col.pack_start(rend[1], rend[2])
4911            if rend[0] != 'avatar':
4912                col.add_attribute(rend[1], rend[3], rend[4])
4913            col.set_cell_data_func(rend[1], rend[5], rend[6])
4914        # set renderers properties
4915        for renderer in self.renderers_propertys:
4916            renderer.set_property(self.renderers_propertys[renderer][0],
4917                self.renderers_propertys[renderer][1])
4918
4919    def query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip):
4920        try:
4921            path = widget.get_path_at_pos(x_pos, y_pos)
4922            row = path[0]
4923            col = path[1]
4924        except TypeError:
4925            self._roster_tooltip.clear_tooltip()
4926            return False
4927        if not row:
4928            self._roster_tooltip.clear_tooltip()
4929            return False
4930
4931        iter_ = None
4932        try:
4933            model = widget.get_model()
4934            iter_ = model.get_iter(row)
4935        except Exception:
4936            self._roster_tooltip.clear_tooltip()
4937            return False
4938
4939        typ = model[iter_][Column.TYPE]
4940        account = model[iter_][Column.ACCOUNT]
4941        jid = model[iter_][Column.JID]
4942        connected_contacts = []
4943
4944        if typ == 'group':
4945            if jid == _('Observers'):
4946                widget.set_tooltip_cell(tooltip, row, col, None)
4947                tooltip.set_text(
4948                    _('Observers can see your status, but you '
4949                      'are not allowed to see theirs'))
4950                return True
4951            return False
4952
4953        if typ in ('contact', 'self_contact'):
4954            contacts = app.contacts.get_contacts(account, jid)
4955
4956            for contact in contacts:
4957                if contact.show not in ('offline', 'error'):
4958                    connected_contacts.append(contact)
4959            if not connected_contacts:
4960                # no connected contacts, show the offline one
4961                connected_contacts = contacts
4962        elif typ == 'groupchat':
4963            connected_contacts = app.contacts.get_contacts(account, jid)
4964        elif typ != 'account':
4965            return False
4966
4967        value, widget = self._roster_tooltip.get_tooltip(
4968            row, connected_contacts, account, typ)
4969        tooltip.set_custom(widget)
4970        return value
4971
4972    def add_actions(self):
4973
4974        actions = [
4975            ('show-roster',
4976             not self.xml.get_object('roster_vbox2').get_no_show_all(),
4977             self.on_show_roster_action),
4978
4979            ('show-offline',
4980             app.settings.get('showoffline'),
4981             self.on_show_offline_contacts_action),
4982
4983            ('show-active',
4984             app.settings.get('show_only_chat_and_online'),
4985             self.on_show_active_contacts_action),
4986
4987            ('show-transports',
4988             app.settings.get('show_transports_group'),
4989             self.on_show_transports_action),
4990        ]
4991
4992        for action in actions:
4993            action_name, variant, func = action
4994            act = Gio.SimpleAction.new_stateful(
4995                action_name, None, GLib.Variant.new_boolean(variant))
4996            act.connect('change-state', func)
4997            self.window.add_action(act)
4998
4999################################################################################
5000###
5001################################################################################
5002
5003    def __init__(self, application):
5004        self.application = application
5005        self.filtering = False
5006        self.starting = False
5007        self.starting_filtering = False
5008        # Number of renderers plugins added
5009        self.nb_ext_renderers = 0
5010        # When we quit, remember if we already saved config once
5011        self.save_done = False
5012
5013        # [icon, name, type, jid, account, editable, mood_pixbuf,
5014        # activity_pixbuf, TUNE_ICON, LOCATION_ICON, avatar_img,
5015        # padlock_pixbuf, visible]
5016        self.columns = [str, str, str, str, str, str, str, str, str,
5017                        Gtk.Image, str, bool]
5018
5019        self.xml = get_builder('roster_window.ui')
5020        self.window = self.xml.get_object('roster_window')
5021        application.add_window(self.window)
5022        self.add_actions()
5023        self.hpaned = self.xml.get_object('roster_hpaned')
5024
5025        app.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned)
5026        app.interface.msg_win_mgr.connect('window-delete',
5027            self.on_message_window_delete)
5028
5029        self.advanced_menus = [] # We keep them to destroy them
5030        if app.settings.get('roster_window_skip_taskbar'):
5031            self.window.set_property('skip-taskbar-hint', True)
5032        self.tree = self.xml.get_object('roster_treeview')
5033        sel = self.tree.get_selection()
5034        sel.set_mode(Gtk.SelectionMode.MULTIPLE)
5035        # sel.connect('changed',
5036        #       self.on_treeview_selection_changed)
5037
5038        self._iters = {}
5039        # for merged mode
5040        self._iters['MERGED'] = {'account': None, 'groups': {}}
5041        # holds a list of (jid, account) tuples
5042        self._last_selected_contact = []
5043        self.transports_state_images = {'16': {}, '32': {}, 'opened': {},
5044            'closed': {}}
5045
5046        self.last_save_dir = None
5047        self.editing_path = None # path of row with cell in edit mode
5048        self.add_new_contact_handler_id = False
5049        self.service_disco_handler_id = False
5050        self.new_chat_menuitem_handler_id = False
5051        self.single_message_menuitem_handler_id = False
5052        self.profile_avatar_menuitem_handler_id = False
5053        #FIXME: When list_accel_closures will be wrapped in pygtk
5054        # no need of this variable
5055        self.have_new_chat_accel = False # Is the "Ctrl+N" shown ?
5056        self.regroup = app.settings.get('mergeaccounts')
5057        self.clicked_path = None # Used remember on which row we clicked
5058        if len(app.connections) < 2:
5059            # Do not merge accounts if only one exists
5060            self.regroup = False
5061        resize_window(self.window,
5062                      app.settings.get('roster_width'),
5063                      app.settings.get('roster_height'))
5064        restore_roster_position(self.window)
5065
5066        # Remove contact from roster when last event opened
5067        # { (contact, account): { backend: boolean }
5068        self.contacts_to_be_removed = {}
5069        app.events.event_removed_subscribe(self.on_event_removed)
5070
5071        # when this value become 0 we quit main application. If it's more than 0
5072        # it means we are waiting for this number of accounts to disconnect
5073        # before quitting
5074        self.quit_on_next_offline = -1
5075
5076        # groups to draw next time we draw groups.
5077        self.groups_to_draw = {}
5078        # accounts to draw next time we draw accounts.
5079        self.accounts_to_draw = []
5080
5081        # Status selector
5082        self._status_selector = StatusSelector()
5083        self.xml.roster_vbox2.add(self._status_selector)
5084
5085        # Enable/Disable checkboxes at start
5086        if app.settings.get('showoffline'):
5087            self.window.lookup_action('show-active').set_enabled(False)
5088
5089        if app.settings.get('show_only_chat_and_online'):
5090            self.window.lookup_action('show-offline').set_enabled(False)
5091
5092        if self.hpaned.get_child2() is None:
5093            self.window.lookup_action('show-roster').set_enabled(False)
5094
5095        # columns
5096        col = Gtk.TreeViewColumn()
5097        # list of renderers with attributes / properties in the form:
5098        # (name, renderer_object, expand?, attribute_name, attribute_value,
5099        # cell_data_func, func_arg)
5100        self.renderers_list = []
5101        self.renderers_propertys = {}
5102
5103        renderer_text = Gtk.CellRendererText()
5104        self.renderers_propertys[renderer_text] = ('ellipsize',
5105            Pango.EllipsizeMode.END)
5106
5107        def add_avatar_renderer():
5108            self.renderers_list.append(('avatar', Gtk.CellRendererPixbuf(),
5109                False, None, Column.AVATAR_IMG,
5110                self._fill_avatar_pixbuf_renderer, None))
5111
5112        if app.settings.get('avatar_position_in_roster') == 'left':
5113            add_avatar_renderer()
5114
5115        self.renderers_list += (
5116                ('icon', Gtk.CellRendererPixbuf(), False,
5117                'icon_name', Column.IMG, self._iconCellDataFunc, None),
5118
5119                ('name', renderer_text, True,
5120                'markup', Column.NAME, self._nameCellDataFunc, None),
5121
5122                ('mood', Gtk.CellRendererPixbuf(), False,
5123                'icon_name', Column.MOOD_PIXBUF,
5124                self._fill_pep_pixbuf_renderer, Column.MOOD_PIXBUF),
5125
5126                ('activity', Gtk.CellRendererPixbuf(), False,
5127                'icon_name', Column.ACTIVITY_PIXBUF,
5128                self._fill_pep_pixbuf_renderer, Column.ACTIVITY_PIXBUF),
5129
5130                ('tune', Gtk.CellRendererPixbuf(), False,
5131                'icon_name', Column.TUNE_ICON,
5132                self._fill_pep_pixbuf_renderer, Column.TUNE_ICON),
5133
5134                ('geoloc', Gtk.CellRendererPixbuf(), False,
5135                'icon_name', Column.LOCATION_ICON,
5136                self._fill_pep_pixbuf_renderer, Column.LOCATION_ICON))
5137
5138        if app.settings.get('avatar_position_in_roster') == 'right':
5139            add_avatar_renderer()
5140
5141        self.renderers_list.append(('padlock', Gtk.CellRendererPixbuf(), False,
5142                'icon_name', Column.PADLOCK_PIXBUF,
5143                self._fill_padlock_pixbuf_renderer, None))
5144
5145        # fill and append column
5146        self.fill_column(col)
5147        self.tree.append_column(col)
5148
5149        # do not show gtk arrows workaround
5150        col = Gtk.TreeViewColumn()
5151        render_pixbuf = Gtk.CellRendererPixbuf()
5152        col.pack_start(render_pixbuf, False)
5153        self.tree.append_column(col)
5154        col.set_visible(False)
5155        self.tree.set_expander_column(col)
5156
5157        # Signals
5158        # Drag
5159        self.tree.enable_model_drag_source(
5160            Gdk.ModifierType.BUTTON1_MASK,
5161            [],
5162            Gdk.DragAction.DEFAULT |
5163            Gdk.DragAction.MOVE |
5164            Gdk.DragAction.COPY)
5165        self.tree.drag_source_add_text_targets()
5166
5167        # Drop
5168        self.tree.enable_model_drag_dest([], Gdk.DragAction.DEFAULT)
5169        self.TARGET_TYPE_URI_LIST = 80
5170        uri_entry = Gtk.TargetEntry.new(
5171            'text/uri-list',
5172            Gtk.TargetFlags.OTHER_APP,
5173            self.TARGET_TYPE_URI_LIST)
5174        dst_targets = Gtk.TargetList.new([uri_entry])
5175        dst_targets.add_text_targets(0)
5176        self.tree.drag_dest_set_target_list(dst_targets)
5177
5178        # Connect
5179        self.tree.connect('drag-begin', self.drag_begin)
5180        self.tree.connect('drag-end', self.drag_end)
5181        self.tree.connect('drag-drop', self.drag_drop)
5182        self.tree.connect('drag-data-get', self.drag_data_get_data)
5183        self.tree.connect('drag-data-received', self.drag_data_received_data)
5184        self.dragging = False
5185        self.xml.connect_signals(self)
5186        self.combobox_callback_active = True
5187
5188        self.collapsed_rows = app.settings.get('collapsed_rows').split('\t')
5189        self.tree.set_has_tooltip(True)
5190        self._roster_tooltip = RosterTooltip()
5191        self.tree.connect('query-tooltip', self.query_tooltip)
5192        # Workaround: For strange reasons signal is behaving like row-changed
5193        self._toggeling_row = False
5194        self.setup_and_draw_roster()
5195
5196        if app.settings.get('show_roster_on_startup') == 'always':
5197            self.window.show_all()
5198        elif app.settings.get('show_roster_on_startup') == 'never':
5199            if app.settings.get('trayicon') != 'always':
5200                # Without trayicon, user should see the roster!
5201                self.window.show_all()
5202                app.settings.set('last_roster_visible', True)
5203        else:
5204            if app.settings.get('last_roster_visible') or \
5205            app.settings.get('trayicon') != 'always':
5206                self.window.show_all()
5207
5208        self.scale_factor = self.window.get_scale_factor()
5209
5210        accounts = app.settings.get_accounts()
5211
5212        if (not accounts or
5213                accounts == ['Local'] and
5214                not app.settings.get_account_setting('Local', 'active')):
5215        # if we have no account configured or only Local account but not enabled
5216            def _open_wizard():
5217                open_window('AccountWizard')
5218
5219            # Open wizard only after roster is created, so we can make it
5220            # transient for the roster window
5221            GLib.idle_add(_open_wizard)
5222
5223        # Setting CTRL+S to be the shortcut to change status message
5224        accel_group = Gtk.AccelGroup()
5225        keyval, mod = Gtk.accelerator_parse('<Control>s')
5226        accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE,
5227            self.accel_group_func)
5228
5229        # Setting CTRL+k to focus rfilter_entry
5230        keyval, mod = Gtk.accelerator_parse('<Control>k')
5231        accel_group.connect(keyval, mod, Gtk.AccelFlags.VISIBLE,
5232            self.accel_group_func)
5233        self.window.add_accel_group(accel_group)
5234
5235        # Setting the search stuff
5236        self.rfilter_entry = self.xml.get_object('rfilter_entry')
5237        self.rfilter_string = ''
5238        self.rfilter_enabled = False
5239        self.rfilter_entry.connect('key-press-event',
5240            self.on_rfilter_entry_key_press_event)
5241
5242        app.ged.register_event_handler('presence-received', ged.GUI1,
5243            self._nec_presence_received)
5244        app.ged.register_event_handler('roster-received', ged.GUI1,
5245            self._nec_roster_received)
5246        app.ged.register_event_handler('anonymous-auth', ged.GUI1,
5247            self._nec_anonymous_auth)
5248        app.ged.register_event_handler('our-show', ged.GUI2,
5249            self._nec_our_show)
5250        app.ged.register_event_handler('connection-type', ged.GUI1,
5251            self._nec_connection_type)
5252        app.ged.register_event_handler('agent-removed', ged.GUI1,
5253            self._nec_agent_removed)
5254        app.ged.register_event_handler('nickname-received', ged.GUI1,
5255            self._on_nickname_received)
5256        app.ged.register_event_handler('mood-received', ged.GUI1,
5257            self._on_mood_received)
5258        app.ged.register_event_handler('activity-received', ged.GUI1,
5259            self._on_activity_received)
5260        app.ged.register_event_handler('tune-received', ged.GUI1,
5261            self._on_tune_received)
5262        app.ged.register_event_handler('location-received', ged.GUI1,
5263            self._on_location_received)
5264        app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
5265            self._nec_update_avatar)
5266        app.ged.register_event_handler('update-room-avatar', ged.GUI1,
5267            self._nec_update_avatar)
5268        app.ged.register_event_handler('muc-subject', ged.GUI1,
5269            self._nec_muc_subject_received)
5270        app.ged.register_event_handler('metacontacts-received', ged.GUI2,
5271            self._nec_metacontacts_received)
5272        app.ged.register_event_handler('signed-in', ged.GUI1,
5273            self._nec_signed_in)
5274        app.ged.register_event_handler('decrypted-message-received', ged.GUI2,
5275            self._nec_decrypted_message_received)
5276        app.ged.register_event_handler('blocking', ged.GUI1,
5277            self._nec_blocking)
5278        app.ged.register_event_handler('style-changed', ged.GUI1,
5279            self._style_changed)
5280        app.ged.register_event_handler('chatstate-received', ged.GUI1,
5281                                       self._nec_chatstate_received)
5282        app.ged.register_event_handler('muc-disco-update', ged.GUI1,
5283                                       self._on_muc_disco_update)
5284        app.ged.register_event_handler('bookmarks-received', ged.GUI2,
5285                                       self._on_bookmarks_received)
5286