groupchat_control.py 118 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
#                    Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
#                         Travis Shirk <travis AT pobox.com>
# Copyright (C) 2007-2008 Julien Pivotto <roidelapluie AT gmail.com>
#                         Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#                    Jonathan Schleifer <js-gajim AT webkeks.org>
11
# Copyright (C) 2018 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
Philipp Hörist's avatar
Philipp Hörist committed
12 13 14 15 16 17 18 19 20 21 22 23 24 25
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
nicfit's avatar
nicfit committed
26

Philipp Hörist's avatar
Philipp Hörist committed
27 28
from typing import Optional

nicfit's avatar
nicfit committed
29
import time
30
import locale
31
import base64
Philipp Hörist's avatar
Philipp Hörist committed
32 33
import logging
from enum import IntEnum, unique
Philipp Hörist's avatar
Philipp Hörist committed
34

Philipp Hörist's avatar
Philipp Hörist committed
35
import nbxmpp
36
from nbxmpp.protocol import JID
Philipp Hörist's avatar
Philipp Hörist committed
37
from nbxmpp.const import StatusCode
Philipp Hörist's avatar
Philipp Hörist committed
38 39 40 41
from nbxmpp.const import Affiliation
from nbxmpp.const import Role
from nbxmpp.const import Error
from nbxmpp.const import PresenceType
42
from nbxmpp.util import is_error_result
43

44
from gi.repository import Gtk
Dicson's avatar
Dicson committed
45
from gi.repository import Gdk
46
from gi.repository import Pango
Yann Leboulanger's avatar
Yann Leboulanger committed
47
from gi.repository import GLib
Philipp Hörist's avatar
Philipp Hörist committed
48
from gi.repository import Gio
Philipp Hörist's avatar
Philipp Hörist committed
49

André's avatar
André committed
50 51 52 53
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import message_control
from gajim import vcard
54

Philipp Hörist's avatar
Philipp Hörist committed
55
from gajim.common.const import AvatarSize
Philipp Hörist's avatar
Philipp Hörist committed
56
from gajim.common.caps_cache import muc_caps_cache
André's avatar
André committed
57
from gajim.common import events
58
from gajim.common import app
André's avatar
André committed
59
from gajim.common import helpers
60
from gajim.common.helpers import open_uri
Philipp Hörist's avatar
Philipp Hörist committed
61
from gajim.common.helpers import event_filter
André's avatar
André committed
62
from gajim.common import ged
63
from gajim.common.i18n import _
64
from gajim.common import contacts
Philipp Hörist's avatar
Philipp Hörist committed
65
from gajim.common.const import StyleAttr
66
from gajim.common.const import Chatstate
67

André's avatar
André committed
68
from gajim.chat_control_base import ChatControlBase
69

André's avatar
André committed
70 71
from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
72

73 74
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationCheckDialog
75 76
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import InputTextDialog
77
from gajim.gtk.dialogs import DestroyMucDialog
78 79
from gajim.gtk.dialogs import InputDialog
from gajim.gtk.dialogs import ChangeNickDialog
Philipp Hörist's avatar
Philipp Hörist committed
80
from gajim.gtk.single_message import SingleMessageWindow
81 82
from gajim.gtk.filechoosers import AvatarChooserDialog
from gajim.gtk.add_contact import AddNewContactWindow
83
from gajim.gtk.tooltips import GCTooltip
84
from gajim.gtk.groupchat_config import GroupchatConfig
85
from gajim.gtk.adhoc import AdHocCommand
86
from gajim.gtk.dataform import DataFormWidget
87
from gajim.gtk.util import NickCompletionGenerator
88
from gajim.gtk.util import get_icon_name
89
from gajim.gtk.util import get_affiliation_surface
90
from gajim.gtk.util import get_builder
91

Philipp Hörist's avatar
Philipp Hörist committed
92

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

95
@unique
96 97
class Column(IntEnum):
    IMG = 0 # image to show state (online, new message etc)
98
    NICK = 1 # contact nickname or ROLE name
99 100
    TYPE = 2 # type of the row ('contact' or 'role')
    TEXT = 3 # text shown in the cellrenderer
101
    AVATAR_IMG = 4 # avatar of the contact
Brendan Taylor's avatar
Brendan Taylor committed
102

103

nicfit's avatar
nicfit committed
104
class GroupchatControl(ChatControlBase):
105 106 107 108 109 110
    TYPE_ID = message_control.TYPE_GC

    # Set a command host to bound to. Every command given through a group chat
    # will be processed with this command host.
    COMMAND_HOST = GroupChatCommands

Philipp Hörist's avatar
Philipp Hörist committed
111
    def __init__(self, parent_win, contact, nick, acct, is_continued=False):
112
        ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
113
            'groupchat_control', contact, acct)
114

115
        self.force_non_minimizable = False
116
        self.is_continued = is_continued
117
        self.is_anonymous = True
118
        self.join_time = 0
119 120 121 122 123 124 125 126 127

        # Controls the state of autorejoin.
        # None - autorejoin is neutral.
        # False - autorejoin is to be prevented (gets reset to initial state in
        #         got_connected()).
        # int - autorejoin is being active and working (gets reset to initial
        #       state in got_connected()).
        self.autorejoin = None

128 129 130
        # Keep error dialog instance to be sure to have only once at a time
        self.error_dialog = None

131 132 133
        # Source id for saving the handle position
        self._handle_timeout_id = None

Philipp Hörist's avatar
Philipp Hörist committed
134 135 136
        self.emoticons_button = self.xml.get_object('emoticons_button')
        self.toggle_emoticons()

137 138 139
        formattings_button = self.xml.get_object('formattings_button')
        formattings_button.set_sensitive(False)

140
        self._state_change_handler_id = None
141
        if parent_win is not None:
142
            # On AutoJoin with minimize Groupchats are created without parent
Philipp Hörist's avatar
Philipp Hörist committed
143
            # Tooltip Window and Actions have to be created with parent
144
            self.set_tooltip()
Philipp Hörist's avatar
Philipp Hörist committed
145
            self.add_actions()
146
            GLib.idle_add(self.update_actions)
147
            self.scale_factor = parent_win.window.get_scale_factor()
148
            self._connect_window_state_change(parent_win)
149 150
        else:
            self.scale_factor = app.interface.roster.scale_factor
lovetox's avatar
lovetox committed
151

152
        widget = self.xml.get_object('list_treeview')
153 154 155
        id_ = widget.connect('row_expanded', self.on_list_treeview_row_expanded)
        self.handlers[id_] = widget

156 157
        id_ = widget.connect('row_collapsed',
            self.on_list_treeview_row_collapsed)
158 159 160
        self.handlers[id_] = widget

        id_ = widget.connect('row_activated',
161
            self.on_list_treeview_row_activated)
162 163 164
        self.handlers[id_] = widget

        id_ = widget.connect('button_press_event',
165
            self.on_list_treeview_button_press_event)
166 167 168
        self.handlers[id_] = widget

        id_ = widget.connect('key_press_event',
169
            self.on_list_treeview_key_press_event)
170 171 172
        self.handlers[id_] = widget

        self.room_jid = self.contact.jid
Philipp Hörist's avatar
Philipp Hörist committed
173
        self.nick = nick
174
        self.new_nick = ''
175 176 177 178

        bm_module = app.connections[self.account].get_module('Bookmarks')
        self.name = bm_module.get_name_from_bookmark(self.room_jid)

Philipp Hörist's avatar
Philipp Hörist committed
179
        self.contact.name = self.name
180 181

        self.widget_set_visible(self.xml.get_object('banner_eventbox'),
182
            app.config.get('hide_groupchat_banner'))
183 184 185 186 187

        # muc attention flag (when we are mentioned in a muc)
        # if True, the room has mentioned us
        self.attention_flag = False

Philipp Hörist's avatar
Philipp Hörist committed
188 189 190
        # True if we initiated room destruction
        self._wait_for_destruction = False

191 192 193
        # sorted list of nicks who mentioned us (last at the end)
        self.attention_list = []
        self.nick_hits = []
194
        self._nick_completion = NickCompletionGenerator(self.nick)
195 196 197 198 199 200 201 202 203
        self.last_key_tabs = False

        self.subject = ''

        self.name_label = self.xml.get_object('banner_name_label')
        self.event_box = self.xml.get_object('banner_eventbox')

        self.list_treeview = self.xml.get_object('list_treeview')
        id_ = self.list_treeview.connect('style-set',
204
                                         self.on_list_treeview_style_set)
205
        self.handlers[id_] = self.list_treeview
206

207 208 209 210 211
        # flag that stops hpaned position event
        # when the handle gets resized in another control
        self._resize_from_another_muc = False

        self.hpaned = self.xml.get_object('hpaned')
Philipp Hörist's avatar
Philipp Hörist committed
212

213
        # set the position of the current hpaned
214
        hpaned_position = app.config.get('gc-hpaned-position')
215 216
        self.hpaned.set_position(hpaned_position)

217 218 219 220 221
        # Holds the Gtk.TreeRowReference for each contact
        self._contact_refs = {}
        # Holds the Gtk.TreeRowReference for each role
        self._role_refs = {}

222
        #status_image, shown_nick, type, nickname, avatar
223
        self.columns = [str, str, str, str, Gtk.Image]
224
        self.model = Gtk.TreeStore(*self.columns)
225
        self.model.set_sort_func(Column.NICK, self.tree_compare_iters)
226 227

        # columns
228
        column = Gtk.TreeViewColumn()
229 230 231 232 233 234 235
        # list of renderers with attributes / properties in the form:
        # (name, renderer_object, expand?, attribute_name, attribute_value,
        # cell_data_func, func_arg)
        self.renderers_list = []
        # Number of renderers plugins added
        self.nb_ext_renderers = 0
        self.renderers_propertys = {}
236
        renderer_text = Gtk.CellRendererText()
237
        self.renderers_propertys[renderer_text] = ('ellipsize',
238
            Pango.EllipsizeMode.END)
239 240 241

        self.renderers_list += (
            # status img
242
            ('icon', Gtk.CellRendererPixbuf(), False,
243
            'icon_name', Column.IMG, self._cell_data_func, 'status'),
244 245
            # contact name
            ('name', renderer_text, True,
246
            'markup', Column.TEXT, self._cell_data_func, 'name'))
247 248

        # avatar img
249 250
        avatar_renderer = ('avatar', Gtk.CellRendererPixbuf(),
            False, None, Column.AVATAR_IMG,
251
            self._cell_data_func, 'avatar')
252

253
        if app.config.get('avatar_position_in_roster') == 'right':
254
            self.renderers_list.append(avatar_renderer)
255
        else:
256
            self.renderers_list.insert(0, avatar_renderer)
257

258
        self.fill_column(column)
259 260 261
        self.list_treeview.append_column(column)

        # workaround to avoid gtk arrows to be shown
262 263
        column = Gtk.TreeViewColumn() # 2nd COLUMN
        renderer = Gtk.CellRendererPixbuf()
Dicson's avatar
Dicson committed
264
        column.pack_start(renderer, False)
265 266 267 268
        self.list_treeview.append_column(column)
        column.set_visible(False)
        self.list_treeview.set_expander_column(column)

Philipp Hörist's avatar
Philipp Hörist committed
269
        self.setup_seclabel()
270

271 272 273 274 275
        # Send file
        self.sendfile_button = self.xml.get_object('sendfile_button')
        self.sendfile_button.set_action_name('win.send-file-' + \
                                             self.control_id)

276 277 278 279 280 281 282 283 284 285 286
        # Encryption
        self.lock_image = self.xml.get_object('lock_image')
        self.authentication_button = self.xml.get_object(
            'authentication_button')
        id_ = self.authentication_button.connect('clicked',
            self._on_authentication_button_clicked)
        self.handlers[id_] = self.authentication_button
        self.set_lock_image()

        self.encryption_menu = self.xml.get_object('encryption_menu')
        self.encryption_menu.set_menu_model(
287
            gui_menu_builder.get_encryption_menu(self.control_id, self.type_id))
288
        self.set_encryption_menu_icon()
289

Philipp Hörist's avatar
Philipp Hörist committed
290 291 292
        # Banner
        self.banner_actionbar = self.xml.get_object('banner_actionbar')
        self.hide_roster_button = Gtk.Button.new_from_icon_name(
293
            'go-next-symbolic', Gtk.IconSize.MENU)
Philipp Hörist's avatar
Philipp Hörist committed
294 295 296 297 298 299 300 301 302 303
        self.hide_roster_button.connect('clicked',
                                        lambda *args: self.show_roster())
        self.subject_button = Gtk.MenuButton()
        self.subject_button.set_image(Gtk.Image.new_from_icon_name(
            'go-down-symbolic', Gtk.IconSize.MENU))
        self.subject_button.set_popover(SubjectPopover())
        self.subject_button.set_no_show_all(True)
        self.banner_actionbar.pack_end(self.hide_roster_button)
        self.banner_actionbar.pack_start(self.subject_button)

Philipp Hörist's avatar
Philipp Hörist committed
304 305 306
        # Holds CaptchaRequest widget
        self._captcha_request = None

Philipp Hörist's avatar
Philipp Hörist committed
307
        # GC Roster tooltip
308
        self.gc_tooltip = GCTooltip()
Philipp Hörist's avatar
Philipp Hörist committed
309

310 311 312
        self.control_menu = gui_menu_builder.get_groupchat_menu(self.control_id,
                                                                self.account,
                                                                self.room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
313 314
        settings_menu = self.xml.get_object('settings_menu')
        settings_menu.set_menu_model(self.control_menu)
Philipp Hörist's avatar
Philipp Hörist committed
315

Philipp Hörist's avatar
Philipp Hörist committed
316 317 318 319 320 321 322 323 324
        self._event_handlers = [
            ('muc-user-joined', ged.GUI1, self._on_user_joined),
            ('muc-user-left', ged.GUI1, self._on_user_left),
            ('muc-nickname-changed', ged.GUI1, self._on_nickname_changed),
            ('muc-self-presence', ged.GUI1, self._on_self_presence),
            ('muc-self-kicked', ged.GUI1, self._on_self_kicked),
            ('muc-user-affiliation-changed', ged.GUI1, self._on_affiliation_changed),
            ('muc-user-status-show-changed', ged.GUI1, self._on_status_show_changed),
            ('muc-user-role-changed', ged.GUI1, self._on_role_changed),
Philipp Hörist's avatar
Philipp Hörist committed
325 326 327 328 329 330
            ('muc-destroyed', ged.GUI1, self._on_destroyed),
            ('muc-presence-error', ged.GUI1, self._on_presence_error),
            ('muc-config-changed', ged.GUI1, self._on_config_changed),
            ('muc-subject', ged.GUI1, self._on_subject),
            ('muc-captcha-challenge', ged.GUI1, self._on_captcha_challenge),
            ('muc-voice-approval', ged.GUI1, self._on_voice_approval),
331 332
            ('muc-disco-update', ged.GUI1, self._on_disco_update),
            ('muc-configuration-finished', ged.GUI1, self._on_configuration_finished),
Philipp Hörist's avatar
Philipp Hörist committed
333 334 335 336 337 338 339 340 341 342 343 344
            ('gc-message-received', ged.GUI1, self._nec_gc_message_received),
            ('mam-decrypted-message-received', ged.GUI1, self._nec_mam_decrypted_message_received),
            ('update-gc-avatar', ged.GUI1, self._nec_update_avatar),
            ('update-room-avatar', ged.GUI1, self._nec_update_room_avatar),
            ('signed-in', ged.GUI1, self._nec_signed_in),
            ('decrypted-message-received', ged.GUI2, self._nec_decrypted_message_received),
            ('gc-stanza-message-outgoing', ged.OUT_POSTCORE, self._message_sent),
        ]

        for handler in self._event_handlers:
            app.ged.register_event_handler(*handler)

345
        self.is_connected = False
346 347 348 349 350
        # disable win, we are not connected yet
        ChatControlBase.got_disconnected(self)

        self.update_ui()
        self.widget.show_all()
351 352 353 354 355

        if app.config.get('hide_groupchat_occupants_list'):
            # Roster is shown by default, so toggle the roster button to hide it
            self.show_roster()

356 357
        # PluginSystem: adding GUI extension point for this GroupchatControl
        # instance object
358
        app.plugin_manager.gui_extension_point('groupchat_control', self)
359

Philipp Hörist's avatar
Philipp Hörist committed
360
    def add_actions(self):
361
        super().add_actions()
Philipp Hörist's avatar
Philipp Hörist committed
362 363 364 365 366 367 368 369
        actions = [
            ('change-subject-', self._on_change_subject),
            ('change-nick-', self._on_change_nick),
            ('disconnect-', self._on_disconnect),
            ('destroy-', self._on_destroy_room),
            ('configure-', self._on_configure_room),
            ('bookmark-', self._on_bookmark_room),
            ('request-voice-', self._on_request_voice),
370
            ('execute-command-', self._on_execute_command),
371
            ('upload-avatar-', self._on_upload_avatar),
372
        ]
Philipp Hörist's avatar
Philipp Hörist committed
373 374 375 376 377 378 379

        for action in actions:
            action_name, func = action
            act = Gio.SimpleAction.new(action_name + self.control_id, None)
            act.connect("activate", func)
            self.parent_win.window.add_action(act)

380 381
        minimize = app.config.get_per(
            'rooms', self.contact.jid, 'minimize_on_close', True)
Philipp Hörist's avatar
Philipp Hörist committed
382 383

        act = Gio.SimpleAction.new_stateful(
384 385 386 387 388 389 390 391 392 393 394 395
            'minimize-on-close-' + self.control_id, None,
            GLib.Variant.new_boolean(minimize))
        act.connect('change-state', self._on_minimize_on_close)
        self.parent_win.window.add_action(act)

        minimize = app.config.get_per(
            'rooms', self.contact.jid, 'minimize_on_autojoin', True)

        act = Gio.SimpleAction.new_stateful(
            'minimize-on-autojoin-' + self.control_id, None,
            GLib.Variant.new_boolean(minimize))
        act.connect('change-state', self._on_minimize_on_autojoin)
Philipp Hörist's avatar
Philipp Hörist committed
396 397
        self.parent_win.window.add_action(act)

398
        default_muc_chatstate = app.config.get('send_chatstate_muc_default')
399
        chatstate = app.config.get_per(
Philipp Hörist's avatar
Philipp Hörist committed
400
            'rooms', self.contact.jid, 'send_chatstate', default_muc_chatstate)
401 402 403 404 405 406 407 408

        act = Gio.SimpleAction.new_stateful(
            'send-chatstate-' + self.control_id,
            GLib.VariantType.new("s"),
            GLib.Variant("s", chatstate))
        act.connect('change-state', self._on_send_chatstate)
        self.parent_win.window.add_action(act)

Philipp Hörist's avatar
Philipp Hörist committed
409 410 411
        # Enable notify on all for private rooms
        members_only = muc_caps_cache.supports(self.contact.jid,
                                               'muc#roomconfig_membersonly')
Philipp Hörist's avatar
Philipp Hörist committed
412
        value = app.config.get_per(
Philipp Hörist's avatar
Philipp Hörist committed
413
            'rooms', self.contact.jid, 'notify_on_all_messages', members_only)
Philipp Hörist's avatar
Philipp Hörist committed
414 415 416 417 418 419 420

        act = Gio.SimpleAction.new_stateful(
            'notify-on-message-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_notify_on_all_messages)
        self.parent_win.window.add_action(act)

421
        status_default = app.config.get('print_status_muc_default')
422 423
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_status', status_default)
Philipp Hörist's avatar
Philipp Hörist committed
424 425 426 427 428 429 430

        act = Gio.SimpleAction.new_stateful(
            'print-status-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_print_status)
        self.parent_win.window.add_action(act)

431
        join_default = app.config.get('print_join_left_default')
432 433
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_join_left', join_default)
Philipp Hörist's avatar
Philipp Hörist committed
434 435 436 437 438 439 440

        act = Gio.SimpleAction.new_stateful(
            'print-join-left-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_print_join_left)
        self.parent_win.window.add_action(act)

441 442 443 444 445 446 447 448 449 450 451
        archive_info = app.logger.get_archive_infos(self.contact.jid)
        threshold = helpers.get_sync_threshold(self.contact.jid,
                                               archive_info)

        inital = GLib.Variant.new_string(str(threshold))
        act = Gio.SimpleAction.new_stateful(
            'choose-sync-' + self.control_id,
            inital.get_type(), inital)
        act.connect('change-state', self._on_sync_threshold)
        self.parent_win.window.add_action(act)

Philipp Hörist's avatar
Philipp Hörist committed
452 453 454
    def update_actions(self):
        if self.parent_win is None:
            return
Philipp Hörist's avatar
Philipp Hörist committed
455

Philipp Hörist's avatar
Philipp Hörist committed
456 457
        contact = app.contacts.get_gc_contact(
            self.account, self.room_jid, self.nick)
Philipp Hörist's avatar
Philipp Hörist committed
458
        con = app.connections[self.account]
Philipp Hörist's avatar
Philipp Hörist committed
459 460

        # Destroy Room
461 462
        self._get_action('destroy-').set_enabled(self.is_connected and
                                                 contact.affiliation.is_owner)
Philipp Hörist's avatar
Philipp Hörist committed
463 464

        # Configure Room
465
        self._get_action('configure-').set_enabled(
Philipp Hörist's avatar
Philipp Hörist committed
466 467
            self.is_connected and contact.affiliation in (Affiliation.ADMIN,
                                                          Affiliation.OWNER))
Philipp Hörist's avatar
Philipp Hörist committed
468 469 470

        # Bookmarks
        con = app.connections[self.account]
Philipp Hörist's avatar
Philipp Hörist committed
471
        bookmarked = con.get_module('Bookmarks').is_bookmark(self.room_jid)
472 473
        self._get_action('bookmark-').set_enabled(self.is_connected and
                                                  not bookmarked)
Philipp Hörist's avatar
Philipp Hörist committed
474 475 476

        # Request Voice
        role = self.get_role(self.nick)
477 478
        self._get_action('request-voice-').set_enabled(self.is_connected and
                                                       role.is_visitor)
Philipp Hörist's avatar
Philipp Hörist committed
479 480

        # Change Subject
481 482 483 484
        subject = False
        if contact is not None:
            subject = muc_caps_cache.is_subject_change_allowed(
                self.room_jid, contact.affiliation)
485 486
        self._get_action('change-subject-').set_enabled(self.is_connected and
                                                        subject)
Philipp Hörist's avatar
Philipp Hörist committed
487 488

        # Change Nick
489
        self._get_action('change-nick-').set_enabled(self.is_connected)
Philipp Hörist's avatar
Philipp Hörist committed
490

491
        # Execute command
492
        self._get_action('execute-command-').set_enabled(self.is_connected)
493

494
        # Send file (HTTP File Upload)
495 496 497 498 499
        httpupload = self._get_action(
            'send-file-httpupload-')
        httpupload.set_enabled(self.is_connected and
                               con.get_module('HTTPUpload').available)
        self._get_action('send-file-').set_enabled(httpupload.get_enabled())
500

501
        if self.is_connected and httpupload.get_enabled():
502
            tooltip_text = _('Send File…')
503 504 505 506
            max_file_size = con.get_module('HTTPUpload').max_file_size
            if max_file_size is not None:
                max_file_size = max_file_size / (1024 * 1024)
                tooltip_text = _('Send File (max. %s MiB)…') % max_file_size
507 508
        else:
            tooltip_text = _('No File Transfer available')
509 510
        self.sendfile_button.set_tooltip_text(tooltip_text)

511 512
        # Upload Avatar
        vcard_support = muc_caps_cache.supports(self.room_jid, nbxmpp.NS_VCARD)
513 514 515 516
        self._get_action('upload-avatar-').set_enabled(
            self.is_connected and
            vcard_support and
            contact.affiliation.is_owner)
517

518 519
        # Sync Threshold
        has_mam = muc_caps_cache.has_mam(self.room_jid)
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
        self._get_action('choose-sync-').set_enabled(has_mam)

        # Print join/left
        join_default = app.config.get('print_join_left_default')
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_join_left', join_default)
        self._get_action('print-join-left-').set_state(
            GLib.Variant.new_boolean(value))

        # Print join/left
        status_default = app.config.get('print_status_muc_default')
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_status', status_default)
        self._get_action('print-status-').set_state(
            GLib.Variant.new_boolean(value))

    def _get_action(self, name):
        win = self.parent_win.window
        return win.lookup_action(name + self.control_id)
539

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
    def _cell_data_func(self, column, renderer, model, iter_, user_data):
        # Background color has to be rendered for all cells
        theme = app.config.get('roster_theme')
        has_parent = bool(model.iter_parent(iter_))
        if has_parent:
            bgcolor = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND)
            renderer.set_property('cell-background', bgcolor)
        else:
            bgcolor = app.css_config.get_value('.gajim-group-row', StyleAttr.BACKGROUND)
            renderer.set_property('cell-background', bgcolor)

        if user_data == 'status':
            self._status_cell_data_func(column, renderer, model, iter_, has_parent)
        elif user_data == 'name':
            self._text_cell_data_func(column, renderer, model, iter_, has_parent, theme)
        elif user_data == 'avatar':
            self._avatar_cell_data_func(column, renderer, model, iter_, has_parent)

    def _status_cell_data_func(self, column, renderer, model, iter_, has_parent):
        renderer.set_property('width', 26)
        icon_name = model[iter_][Column.IMG]
        if ':' in icon_name:
            icon_name, affiliation = icon_name.split(':')
            surface = get_affiliation_surface(
                icon_name, affiliation, self.scale_factor)
            renderer.set_property('icon_name', None)
            renderer.set_property('surface', surface)
        else:
            renderer.set_property('surface', None)
            renderer.set_property('icon_name', icon_name)

    def _avatar_cell_data_func(self, column, renderer, model, iter_, has_parent):
        image = model[iter_][Column.AVATAR_IMG]
        if image is None:
            renderer.set_property('surface', None)
        else:
            surface = image.get_property('surface')
            renderer.set_property('surface', surface)

        renderer.set_property('xalign', 0.5)
        if has_parent:
            renderer.set_property('visible', True)
            renderer.set_property('width', AvatarSize.ROSTER)
        else:
            renderer.set_property('visible', False)

    def _text_cell_data_func(self, column, renderer, model, iter_, has_parent, theme):
        # cell data func is global, because we don't want it to keep
        # reference to GroupchatControl instance (self)
        if has_parent:
            color = app.css_config.get_value('.gajim-contact-row', StyleAttr.COLOR)
            renderer.set_property('foreground', color)
            desc = app.css_config.get_font('.gajim-contact-row')
            renderer.set_property('font-desc', desc)
        else:
            color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR)
            renderer.set_property('foreground', color)
            desc = app.css_config.get_font('.gajim-group-row')
            renderer.set_property('font-desc', desc)

600 601
    @event_filter(['account', 'room_jid'])
    def _on_disco_update(self, _event):
602 603 604 605 606 607 608 609 610 611 612 613 614 615
        if self.parent_win is None:
            return
        win = self.parent_win.window
        self.update_actions()

        # After the room has been created, reevaluate threshold
        if muc_caps_cache.has_mam(self.contact.jid):
            archive_info = app.logger.get_archive_infos(self.contact.jid)
            threshold = helpers.get_sync_threshold(self.contact.jid,
                                                   archive_info)
            win.change_action_state('choose-sync-%s' % self.control_id,
                                    GLib.Variant('s', str(threshold)))


616 617 618 619 620 621
    def _connect_window_state_change(self, parent_win):
        if self._state_change_handler_id is None:
            id_ = parent_win.window.connect('notify::is-maximized',
                                            self._on_window_state_change)
            self._state_change_handler_id = id_

Philipp Hörist's avatar
Philipp Hörist committed
622 623 624 625 626 627
    # Actions

    def _on_change_subject(self, action, param):
        def on_ok(subject):
            # Note, we don't update self.subject since we don't know whether it
            # will work yet
628 629
            con = app.connections[self.account]
            con.get_module('MUC').set_subject(self.room_jid, subject)
Philipp Hörist's avatar
Philipp Hörist committed
630

631
        InputTextDialog(_('Changing Subject'),
Philipp Hörist's avatar
Philipp Hörist committed
632 633 634 635 636 637 638 639 640 641
            _('Please specify the new subject:'), input_str=self.subject,
            ok_handler=on_ok, transient_for=self.parent_win.window)

    def _on_change_nick(self, action, param):
        if 'change_nick_dialog' in app.interface.instances:
            app.interface.instances['change_nick_dialog'].dialog.present()
        else:
            title = _('Changing Nickname')
            prompt = _('Please specify the new nickname you want to use:')
            app.interface.instances['change_nick_dialog'] = \
642
                ChangeNickDialog(self.account, self.room_jid, title,
Philipp Hörist's avatar
Philipp Hörist committed
643 644 645
                prompt, change_nick=True, transient_for=self.parent_win.window)

    def _on_disconnect(self, action, param):
646
        app.connections[self.account].get_module('MUC').leave(self.room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
647 648 649 650 651
        self.force_non_minimizable = True
        self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND)
        self.force_non_minimizable = False

    def _on_destroy_room(self, action, param):
652
        def _on_confirm(reason, jid):
Philipp Hörist's avatar
Philipp Hörist committed
653 654 655 656 657
            if jid:
                # Test jid
                try:
                    jid = helpers.parse_jid(jid)
                except Exception:
658 659 660
                    ErrorDialog(
                        _('Invalid group chat JID'),
                        _('The group chat JID has not allowed characters.'))
Philipp Hörist's avatar
Philipp Hörist committed
661
                    return
Philipp Hörist's avatar
Philipp Hörist committed
662 663

            self._wait_for_destruction = True
664 665
            con = app.connections[self.account]
            con.get_module('MUC').destroy(self.room_jid, reason, jid)
Philipp Hörist's avatar
Philipp Hörist committed
666

667 668
        # Ask for a reason (and an alternate venue)
        DestroyMucDialog(self.room_jid, destroy_handler=_on_confirm)
Philipp Hörist's avatar
Philipp Hörist committed
669

670
    def _on_configure_room(self, _action, _param):
671 672 673 674 675
        win = app.get_app_window('GroupchatConfig', self.account, self.room_jid)
        if win is not None:
            win.present()
            return

676
        contact = app.contacts.get_gc_contact(
Philipp Hörist's avatar
Philipp Hörist committed
677
            self.account, self.room_jid, self.nick)
Philipp Hörist's avatar
Philipp Hörist committed
678
        if contact.affiliation.is_owner:
679
            con = app.connections[self.account]
680 681
            con.get_module('MUC').request_config(
                self.room_jid, callback=self._on_configure_form_received)
Philipp Hörist's avatar
Philipp Hörist committed
682
        elif contact.affiliation.is_admin:
683 684 685 686 687 688 689 690 691
            GroupchatConfig(self.account,
                            self.room_jid,
                            contact.affiliation.value)

    def _on_configure_form_received(self, result):
        if is_error_result(result):
            log.info('Error %s %s', result.jid, result)
            return
        GroupchatConfig(self.account, result.jid, 'owner', result.form)
Philipp Hörist's avatar
Philipp Hörist committed
692 693 694 695 696 697 698 699 700 701

    def _on_print_join_left(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'print_join_left', param.get_boolean())

    def _on_print_status(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'print_status', param.get_boolean())
Philipp Hörist's avatar
Philipp Hörist committed
702 703 704 705 706 707

    def _on_bookmark_room(self, action, param):
        """
        Bookmark the room, without autojoin and not minimized
        """
        password = app.gc_passwords.get(self.room_jid, '')
708
        con = app.connections[self.account]
709
        con.get_module('Bookmarks').add_bookmark(self.name,
710
                                                 JID(self.room_jid),
711 712 713
                                                 True,
                                                 password,
                                                 self.nick)
Philipp Hörist's avatar
Philipp Hörist committed
714
        self.update_actions()
Philipp Hörist's avatar
Philipp Hörist committed
715 716 717 718 719

    def _on_request_voice(self, action, param):
        """
        Request voice in the current room
        """
Philipp Hörist's avatar
Philipp Hörist committed
720 721
        con = app.connections[self.account]
        con.get_module('MUC').request_voice(self.room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
722

723
    def _on_minimize_on_close(self, action, param):
Philipp Hörist's avatar
Philipp Hörist committed
724
        action.set_state(param)
Philipp Hörist's avatar
Philipp Hörist committed
725 726
        app.config.set_per('rooms', self.contact.jid,
                           'minimize_on_close', param.get_boolean())
Philipp Hörist's avatar
Philipp Hörist committed
727

728 729
    def _on_minimize_on_autojoin(self, action, param):
        action.set_state(param)
Philipp Hörist's avatar
Philipp Hörist committed
730 731
        app.config.set_per('rooms', self.contact.jid,
                           'minimize_on_autojoin', param.get_boolean())
Philipp Hörist's avatar
Philipp Hörist committed
732

733 734 735 736 737
    def _on_send_chatstate(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'send_chatstate', param.get_string())

Philipp Hörist's avatar
Philipp Hörist committed
738 739 740 741 742
    def _on_notify_on_all_messages(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'notify_on_all_messages', param.get_boolean())

743 744 745 746 747
    def _on_sync_threshold(self, action, param):
        threshold = param.get_string()
        action.set_state(param)
        app.logger.set_archive_infos(self.contact.jid, sync_threshold=threshold)

748 749 750 751
    def _on_execute_command(self, action, param):
        """
        Execute AdHoc commands on the current room
        """
752
        AdHocCommand(self.account, self.room_jid)
753

754 755
    def _on_upload_avatar(self, action, param):
        def _on_accept(filename):
Philipp Hörist's avatar
Philipp Hörist committed
756 757
            data, sha = app.interface.avatar_storage.prepare_for_publish(
                filename)
758
            if sha is None:
759
                ErrorDialog(
760 761 762 763
                    _('Could not load image'),
                    transient_for=self.parent_win.window)
                return

Philipp Hörist's avatar
Philipp Hörist committed
764
            avatar = base64.b64encode(data).decode('utf-8')
765 766
            con = app.connections[self.account]
            con.get_module('VCardTemp').upload_room_avatar(
767 768 769 770 771 772
                self.room_jid, avatar)

        AvatarChooserDialog(_on_accept,
                            transient_for=self.parent_win.window,
                            modal=True)

Philipp Hörist's avatar
Philipp Hörist committed
773 774 775 776 777 778 779 780 781 782
    def show_roster(self):
        new_state = not self.hpaned.get_child2().is_visible()
        image = self.hide_roster_button.get_image()
        if new_state:
            self.hpaned.get_child2().show()
            image.set_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU)
        else:
            self.hpaned.get_child2().hide()
            image.set_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU)

783 784
    def on_groupchat_maximize(self):
        self.set_tooltip()
Philipp Hörist's avatar
Philipp Hörist committed
785 786
        self.add_actions()
        self.update_actions()
787
        self.set_lock_image()
788
        self._connect_window_state_change(self.parent_win)
789

790 791 792 793 794 795 796 797
    def set_tooltip(self):
        widget = self.xml.get_object('list_treeview')
        if widget.get_tooltip_window():
            return
        widget.set_has_tooltip(True)
        id_ = widget.connect('query-tooltip', self.query_tooltip)
        self.handlers[id_] = widget

lovetox's avatar
lovetox committed
798 799 800 801
    def query_tooltip(self, widget, x_pos, y_pos, keyboard_mode, tooltip):
        try:
            row = self.list_treeview.get_path_at_pos(x_pos, y_pos)[0]
        except TypeError:
Philipp Hörist's avatar
Philipp Hörist committed
802
            self.gc_tooltip.clear_tooltip()
lovetox's avatar
lovetox committed
803 804
            return False
        if not row:
Philipp Hörist's avatar
Philipp Hörist committed
805
            self.gc_tooltip.clear_tooltip()
lovetox's avatar
lovetox committed
806 807 808 809 810 811
            return False

        iter_ = None
        try:
            iter_ = self.model.get_iter(row)
        except Exception:
Philipp Hörist's avatar
Philipp Hörist committed
812
            self.gc_tooltip.clear_tooltip()
lovetox's avatar
lovetox committed
813 814
            return False

815 816
        typ = self.model[iter_][Column.TYPE]
        nick = self.model[iter_][Column.NICK]
lovetox's avatar
lovetox committed
817 818

        if typ != 'contact':
Philipp Hörist's avatar
Philipp Hörist committed
819
            self.gc_tooltip.clear_tooltip()
lovetox's avatar
lovetox committed
820 821
            return False

822
        contact = app.contacts.get_gc_contact(
lovetox's avatar
lovetox committed
823 824
            self.account, self.room_jid, nick)
        if not contact:
Philipp Hörist's avatar
Philipp Hörist committed
825
            self.gc_tooltip.clear_tooltip()
lovetox's avatar
lovetox committed
826 827
            return False

Philipp Hörist's avatar
Philipp Hörist committed
828 829 830
        value, widget = self.gc_tooltip.get_tooltip(contact)
        tooltip.set_custom(widget)
        return value
lovetox's avatar
lovetox committed
831

832 833
    def fill_column(self, col):
        for rend in self.renderers_list:
Dicson's avatar
Dicson committed
834
            col.pack_start(rend[1], rend[2])
835
            if rend[0] not in ('avatar', 'icon'):
836
                col.add_attribute(rend[1], rend[3], rend[4])
837
            col.set_cell_data_func(rend[1], rend[5], rend[6])
Alexander Krotov's avatar
Alexander Krotov committed
838
        # set renderers properties
839
        for renderer in self.renderers_propertys:
840 841 842
            renderer.set_property(self.renderers_propertys[renderer][0],
                self.renderers_propertys[renderer][1])

843
    def tree_compare_iters(self, model, iter1, iter2, data=None):
844
        """
845
        Compare two iterators to sort them
846
        """
847 848
        type1 = model[iter1][Column.TYPE]
        type2 = model[iter2][Column.TYPE]
849 850
        if not type1 or not type2:
            return 0
851 852
        nick1 = model[iter1][Column.NICK]
        nick2 = model[iter2][Column.NICK]
853 854 855 856 857
        if not nick1 or not nick2:
            return 0
        if type1 == 'role':
            return locale.strcoll(nick1, nick2)
        if type1 == 'contact':
858
            gc_contact1 = app.contacts.get_gc_contact(self.account,
859 860 861 862
                    self.room_jid, nick1)
            if not gc_contact1:
                return 0
        if type2 == 'contact':
863
            gc_contact2 = app.contacts.get_gc_contact(self.account,
864 865 866 867
                    self.room_jid, nick2)
            if not gc_contact2:
                return 0
        if type1 == 'contact' and type2 == 'contact' and \
868
        app.config.get('sort_by_show_in_muc'):
Philipp Hörist's avatar
Philipp Hörist committed
869 870 871
            cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4}
            show1 = cshow[gc_contact1.show.value]
            show2 = cshow[gc_contact2.show.value]
872 873
            if show1 < show2:
                return -1
874
            if show1 > show2:
875 876 877 878 879 880 881 882 883 884 885 886
                return 1
        # We compare names
        name1 = gc_contact1.get_shown_name()
        name2 = gc_contact2.get_shown_name()
        return locale.strcoll(name1.lower(), name2.lower())

    def on_msg_textview_populate_popup(self, textview, menu):
        """
        Override the default context menu and we prepend Clear
        and the ability to insert a nick
        """
        ChatControlBase.on_msg_textview_populate_popup(self, textview, menu)
Dicson's avatar
Dicson committed
887
        item = Gtk.SeparatorMenuItem.new()
888 889
        menu.prepend(item)

890
        item = Gtk.MenuItem.new_with_label(_('Insert Nickname'))
891
        menu.prepend(item)
892
        submenu = Gtk.Menu()
893 894
        item.set_submenu(submenu)

895
        for nick in sorted(app.contacts.get_nick_list(self.account,
896
        self.room_jid)):
897 898
            item = Gtk.MenuItem.new_with_label(nick)
            item.set_use_underline(False)
899
            submenu.append(item)
900 901
            id_ = item.connect('activate', self.append_nick_in_msg_textview,
                nick)
902 903 904 905
            self.handlers[id_] = item

        menu.show_all()

906
    def resize_occupant_treeview(self, position):
907 908 909
        if self.hpaned.get_position() == position:
            return
        self._resize_from_another_muc = True
910
        self.hpaned.set_position(position)
911 912
        def _reset_flag():
            self._resize_from_another_muc = False
913 914
        # Reset the flag when everything will be redrawn, and in particular when
        # on_treeview_size_allocate will have been called.
915
        GLib.timeout_add(500, _reset_flag)
916

917 918 919 920 921 922 923 924
    def _on_window_state_change(self, win, param):
        # Add with timeout, because state change happens before
        # the hpaned notifys us about a new handle position
        GLib.timeout_add(100, self._check_for_resize)

    def _on_hpaned_release_button(self, hpaned, event):
        if event.get_button()[1] != 1:
            # We want only to catch the left mouse button
925
            return
926
        self._check_for_resize()
927

928 929 930 931 932 933 934 935 936 937
    def _check_for_resize(self):
        # Check if we have a new position
        pos = self.hpaned.get_position()
        if pos == app.config.get('gc-hpaned-position'):
            return

        # Save new position
        self._remove_handle_timeout()
        app.config.set('gc-hpaned-position', pos)
        # Resize other MUC rosters
938 939 940 941
        for account in app.gc_connected:
            for room_jid in [i for i in app.gc_connected[account] if \
            app.gc_connected[account][i] and i != self.room_jid]:
                ctrl = app.interface.msg_win_mgr.get_gc_control(room_jid,
942 943
                    account)
                if not ctrl and room_jid in \
944 945 946
                app.interface.minimized_controls[account]:
                    ctrl = app.interface.minimized_controls[account][room_jid]
                if ctrl and app.config.get('one_message_window') != 'never':
947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968
                    ctrl.resize_occupant_treeview(pos)

    def _on_hpaned_handle_change(self, hpaned, param):
        if self._resize_from_another_muc:
            return
        # Window was resized, save new handle pos
        pos = hpaned.get_position()
        if pos != app.config.get('gc-hpaned-position'):
            self._remove_handle_timeout(renew=True)

    def _remove_handle_timeout(self, renew=False):
        if self._handle_timeout_id is not None:
            GLib.source_remove(self._handle_timeout_id)
            self._handle_timeout_id = None
        if renew:
            pos = self.hpaned.get_position()
            self._handle_timeout_id = GLib.timeout_add_seconds(
                2, self._save_handle_position, pos)

    def _save_handle_position(self, pos):
        self._handle_timeout_id = None
        app.config.set('gc-hpaned-position', pos)
969 970 971 972 973

    def iter_contact_rows(self):
        """
        Iterate over all contact rows in the tree model
        """
974
        role_iter = self.model.get_iter_first()
975
        while role_iter:
976
            contact_iter = self.model.iter_children(role_iter)
977
            while contact_iter:
978 979 980
                yield self.model[contact_iter]
                contact_iter = self.model.iter_next(contact_iter)
            role_iter = self.model.iter_next(role_iter)
981 982 983 984 985 986 987

    def on_list_treeview_style_set(self, treeview, style):
        """
        When style (theme) changes, redraw all contacts
        """
        # Get the room_jid from treeview
        for contact in self.iter_contact_rows():
988
            nick = contact[Column.NICK]
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002
            self.draw_contact(nick)

    def get_tab_label(self, chatstate):
        """
        Markup the label if necessary. Returns a tuple such as: (new_label_str,
        color) either of which can be None if chatstate is given that means we
        have HE SENT US a chatstate
        """

        has_focus = self.parent_win.window.get_property('has-toplevel-focus')
        current_tab = self.parent_win.get_active_control() == self
        color = None
        if chatstate == 'attention' and (not has_focus or not current_tab):
            self.attention_flag = True
Philipp Hörist's avatar
Philipp Hörist committed
1003
            color = 'tab-muc-directed-msg'
1004 1005 1006 1007 1008 1009
        elif chatstate == 'active' or (current_tab and has_focus):
            self.attention_flag = False
            # get active color from gtk
            color = 'active'
        elif chatstate == 'newmsg' and (not has_focus or not current_tab) \
        and not self.attention_flag:
Philipp Hörist's avatar
Philipp Hörist committed
1010
            color = 'tab-muc-msg'
1011 1012 1013 1014 1015 1016

        if self.is_continued:
            # if this is a continued conversation
            label_str = self.get_continued_conversation_name()
        else:
            label_str = self.name
1017
        label_str = GLib.markup_escape_text(label_str)
1018 1019 1020 1021 1022 1023 1024

        # count waiting highlighted messages
        unread = ''
        num_unread = self.get_nb_unread()
        if num_unread == 1:
            unread = '*'
        elif num_unread > 1:
Yann Leboulanger's avatar
Yann Leboulanger committed
1025
            unread = '[' + str(num_unread) + ']'
1026 1027 1028 1029 1030
        label_str = unread + label_str
        return (label_str, color)

    def get_tab_image(self, count_unread=True):
        tab_image = None
1031
        if self.is_connected:
1032
            tab_image = get_icon_name('muc-active')
1033
        else:
1034
            tab_image = get_icon_name('muc-inactive')
1035 1036 1037 1038
        return tab_image

    def update_ui(self):
        ChatControlBase.update_ui(self)
1039
        for nick in app.contacts.get_nick_list(self.account, self.room_jid):
1040 1041
            self.draw_contact(nick)

1042
    def set_lock_image(self):
1043
        encryption_state = {'visible': self.encryption is not None,
1044 1045 1046
                            'enc_type': self.encryption,
                            'authenticated': False}

1047
        if self.encryption:
1048
            app.plugin_manager.extension_point(
1049
                'encryption_state' + self.encryption, self, encryption_state)
1050 1051 1052

        self._show_lock_image(**encryption_state)

1053
    def _show_lock_image(self, visible, enc_type='', authenticated=False):
1054 1055 1056 1057 1058
        """
        Set lock icon visibility and create tooltip
        """
        if authenticated:
            authenticated_string = _('and authenticated')
1059
            self.lock_image.set_from_icon_name(
1060
                'security-high-symbolic', Gtk.IconSize.MENU)
1061 1062
        else:
            authenticated_string = _('and NOT authenticated')
1063
            self.lock_image.set_from_icon_name(
1064
                'security-low-symbolic', Gtk.IconSize.MENU)
1065 1066 1067 1068 1069 1070 1071 1072 1073

        tooltip = _('%(type)s encryption is active %(authenticated)s.') % {
            'type': enc_type, 'authenticated': authenticated_string}

        self.authentication_button.set_tooltip_text(tooltip)
        self.widget_set_visible(self.authentication_button, not visible)
        self.lock_image.set_sensitive(visible)

    def _on_authentication_button_clicked(self, widget):
1074
        app.plugin_manager.extension_point(
1075 1076
            'encryption_dialog' + self.encryption, self)

1077
    def _change_style(self, model, path, iter_, option):
1078
        model[iter_][Column.NICK] = model[iter_][Column.NICK]
1079 1080

    def change_roster_style(self):
Dicson's avatar
Dicson committed
1081
        self.model.foreach(self._change_style, None)
1082 1083 1084 1085 1086 1087

    def repaint_themed_widgets(self):
        ChatControlBase.repaint_themed_widgets(self)
        self.change_roster_style()

    def _update_banner_state_image(self):
1088
        banner_status_img = self.xml.get_object('gc_banner_status_image')
1089
        if self.is_connected:
Philipp Hörist's avatar
Philipp Hörist committed
1090
            if self.contact.avatar_sha:
Daniel Brötzmann's avatar
Daniel Brötzmann committed
1091
                surface = app.interface.get_avatar(self.contact,
Philipp Hörist's avatar
Philipp Hörist committed
1092 1093 1094 1095
                                                   AvatarSize.ROSTER,
                                                   self.scale_factor)