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

26
import os
27
import time
28
from tempfile import TemporaryDirectory
29

30 31 32
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
Philipp Hörist's avatar
Philipp Hörist committed
33
from gi.repository import Gio
34
from gi.repository import GObject
Philipp Hörist's avatar
Philipp Hörist committed
35

André's avatar
André committed
36
from gajim.common import events
37
from gajim.common import app
André's avatar
André committed
38 39
from gajim.common import helpers
from gajim.common import ged
40
from gajim.common import i18n
41
from gajim.common.i18n import _
42
from gajim.common.helpers import AdditionalDataDict
André's avatar
André committed
43 44
from gajim.common.contacts import GC_Contact
from gajim.common.connection_handlers_events import MessageOutgoingEvent
45
from gajim.common.const import Chatstate
46

47 48 49 50 51 52 53
from gajim import gtkgui_helpers
from gajim import message_control

from gajim.message_control import MessageControl
from gajim.conversation_textview import ConversationTextview
from gajim.message_textview import MessageTextView

54
from gajim.gtk.dialogs import DialogButton
55 56
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.dialogs import NewConfirmationCheckDialog
Philipp Hörist's avatar
Philipp Hörist committed
57 58
from gajim.gtk import util
from gajim.gtk.util import at_the_end
Philipp Hörist's avatar
Philipp Hörist committed
59 60
from gajim.gtk.util import get_show_in_roster
from gajim.gtk.util import get_show_in_systray
61
from gajim.gtk.util import get_hardware_key_codes
62 63
from gajim.gtk.emoji_chooser import emoji_chooser

André's avatar
André committed
64 65
from gajim.command_system.implementation.middleware import ChatCommandProcessor
from gajim.command_system.implementation.middleware import CommandTools
66

67 68 69 70
# The members of these modules are not referenced directly anywhere in this
# module, but still they need to be kept around. Importing them automatically
# registers the contained CommandContainers with the command system, thereby
# populating the list of available commands.
71
# pylint: disable=unused-import
André's avatar
André committed
72 73
from gajim.command_system.implementation import standard
from gajim.command_system.implementation import execute
74
# pylint: enable=unused-import
75

76
if app.is_installed('GSPELL'):
77
    from gi.repository import Gspell  # pylint: disable=ungrouped-imports
Philipp Hörist's avatar
Philipp Hörist committed
78

79 80 81 82 83 84 85

################################################################################
class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
    """
    A base class containing a banner, ConversationTextview, MessageTextView
    """

86 87 88 89 90 91 92
    # This is needed so copying text from the conversation textview
    # works with different language layouts. Pressing the key c on a russian
    # layout yields another keyval than with the english layout.
    # So we match hardware keycodes instead of keyvals.
    # Multiple hardware keycodes can trigger a keyval like Gdk.KEY_c.
    keycodes_c = get_hardware_key_codes(Gdk.KEY_c)

93 94 95 96 97
    def get_nb_unread(self):
        jid = self.contact.jid
        if self.resource:
            jid += '/' + self.resource
        type_ = self.type_id
98
        return len(app.events.get_events(self.account, jid, ['printed_' + type_,
99 100 101 102 103 104 105 106 107 108
                type_]))

    def draw_banner(self):
        """
        Draw the fat line at the top of the window that houses the icon, jid, etc

        Derived types MAY implement this.
        """
        self.draw_banner_text()
        self._update_banner_state_image()
109
        app.plugin_manager.gui_extension_point('chat_control_base_draw_banner',
110 111 112 113 114 115 116
            self)

    def update_toolbar(self):
        """
        update state of buttons in toolbar
        """
        self._update_toolbar()
117
        app.plugin_manager.gui_extension_point(
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
            'chat_control_base_update_toolbar', self)

    def draw_banner_text(self):
        """
        Derived types SHOULD implement this
        """

    def update_ui(self):
        """
        Derived types SHOULD implement this
        """
        self.draw_banner()

    def repaint_themed_widgets(self):
        """
        Derived types MAY implement this
        """
        self.draw_banner()

    def _update_banner_state_image(self):
        """
        Derived types MAY implement this
        """

    def _update_toolbar(self):
        """
        Derived types MAY implement this
        """

    def _nec_our_status(self, obj):
        if self.account != obj.conn.name:
            return
        if obj.show == 'offline' or (obj.show == 'invisible' and \
        obj.conn.is_zeroconf):
            self.got_disconnected()
        else:
            # Other code rejoins all GCs, so we don't do it here
            if not self.type_id == message_control.TYPE_GC:
                self.got_connected()
        if self.parent_win:
            self.parent_win.redraw_tab(self)

Philipp Hörist's avatar
Philipp Hörist committed
160
    def setup_seclabel(self):
161 162
        self.xml.label_selector.hide()
        self.xml.label_selector.set_no_show_all(True)
163
        lb = Gtk.ListStore(str)
164
        self.xml.label_selector.set_model(lb)
165 166
        cell = Gtk.CellRendererText()
        cell.set_property('xpad', 5)  # padding for status text
167
        self.xml.label_selector.pack_start(cell, True)
168
        # text to show is in in first column of liststore
169
        self.xml.label_selector.add_attribute(cell, 'text', 0)
170
        con = app.connections[self.account]
171 172 173
        jid = self.contact.jid
        if self.TYPE_ID == 'pm':
            jid = self.gc_contact.room_jid
174
        if con.get_module('SecLabels').supported:
175
            con.get_module('SecLabels').request_catalog(jid)
176

177 178 179
    def _sec_labels_received(self, event):
        if event.account != self.account:
            return
180 181 182 183 184 185

        jid = self.contact.jid
        if self.TYPE_ID == 'pm':
            jid = self.gc_contact.room_jid

        if event.jid != jid:
186
            return
187
        model = self.xml.label_selector.get_model()
188 189
        model.clear()

190
        sel = 0
191
        _label, labellist, default = event.catalog
192 193
        for index, label in enumerate(labellist):
            model.append([label])
194
            if label == default:
195 196
                sel = index

197 198 199
        self.xml.label_selector.set_active(sel)
        self.xml.label_selector.set_no_show_all(False)
        self.xml.label_selector.show_all()
200 201 202 203 204 205 206 207 208 209

    def __init__(self, type_id, parent_win, widget_name, contact, acct,
    resource=None):
        # Undo needs this variable to know if space has been pressed.
        # Initialize it to True so empty textview is saved in undo list
        self.space_pressed = True

        if resource is None:
            # We very likely got a contact with a random resource.
            # This is bad, we need the highest for caps etc.
210
            c = app.contacts.get_contact_with_highest_priority(acct,
211 212 213 214 215 216 217
                contact.jid)
            if c and not isinstance(c, GC_Contact):
                contact = c

        MessageControl.__init__(self, type_id, parent_win, widget_name,
            contact, acct, resource=resource)

Philipp Hörist's avatar
Philipp Hörist committed
218 219
        if self.TYPE_ID != message_control.TYPE_GC:
            # Create banner and connect signals
220 221
            id_ = self.xml.banner_eventbox.connect(
                'button-press-event',
Philipp Hörist's avatar
Philipp Hörist committed
222
                self._on_banner_eventbox_button_press_event)
223
            self.handlers[id_] = self.xml.banner_eventbox
224

225 226 227
        if self.xml.banner_label is not None:
            id_ = self.xml.banner_label.connect(
                'populate_popup',
Philipp Hörist's avatar
Philipp Hörist committed
228
                self.on_banner_label_populate_popup)
229
            self.handlers[id_] = self.xml.banner_label
230 231 232

        # Init DND
        self.TARGET_TYPE_URI_LIST = 80
233 234 235 236 237 238 239 240 241 242 243
        uri_entry = Gtk.TargetEntry.new(
            'text/uri-list',
            Gtk.TargetFlags.OTHER_APP,
            self.TARGET_TYPE_URI_LIST)
        dst_targets = Gtk.TargetList.new([uri_entry])
        dst_targets.add_text_targets(0)
        self.dnd_list = [uri_entry,
                         Gtk.TargetEntry.new(
                             'MY_TREE_MODEL_ROW',
                             Gtk.TargetFlags.SAME_APP,
                             0)]
244
        id_ = self.widget.connect('drag_data_received',
245
                                  self._on_drag_data_received)
246
        self.handlers[id_] = self.widget
247 248 249 250 251
        self.widget.drag_dest_set(
            Gtk.DestDefaults.ALL,
            self.dnd_list,
            Gdk.DragAction.COPY)
        self.widget.drag_dest_set_target_list(dst_targets)
252 253 254 255 256

        # Create textviews and connect signals
        self.conv_textview = ConversationTextview(self.account)
        id_ = self.conv_textview.connect('quote', self.on_quote)
        self.handlers[id_] = self.conv_textview.tv
257

258 259 260
        # FIXME: DND on non editable TextView, find a better way
        self.drag_entered = False
        id_ = self.conv_textview.tv.connect('drag_data_received',
261
                                            self._on_drag_data_received)
262 263 264 265 266
        self.handlers[id_] = self.conv_textview.tv
        id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion)
        self.handlers[id_] = self.conv_textview.tv
        id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave)
        self.handlers[id_] = self.conv_textview.tv
267 268 269 270 271
        self.conv_textview.tv.drag_dest_set(
            Gtk.DestDefaults.ALL,
            self.dnd_list,
            Gdk.DragAction.COPY)
        self.conv_textview.tv.drag_dest_set_target_list(dst_targets)
272

273 274 275 276
        id_ = self.conv_textview.tv.connect('grab-focus',
                                            self._on_html_textview_grab_focus)
        self.handlers[id_] = self.conv_textview.tv

277
        self.conv_scrolledwindow = self.xml.conversation_scrolledwindow
278 279 280 281 282
        self.conv_scrolledwindow.add(self.conv_textview.tv)
        widget = self.conv_scrolledwindow.get_vadjustment()
        id_ = widget.connect('changed',
            self.on_conversation_vadjustment_changed)
        self.handlers[id_] = widget
283 284 285 286 287 288

        vscrollbar = self.conv_scrolledwindow.get_vscrollbar()
        id_ = vscrollbar.connect('button-release-event',
                                 self._on_scrollbar_button_release)
        self.handlers[id_] = vscrollbar

289 290 291 292 293
        self.correcting = False
        self.last_sent_msg = None

        # add MessageTextView to UI and connect signals
        self.msg_textview = MessageTextView()
294
        self.msg_scrolledwindow = ScrolledWindow()
295
        self.msg_scrolledwindow.add(self.msg_textview)
296

297
        self.xml.hbox.pack_start(self.msg_scrolledwindow, True, True, 0)
298

299 300 301
        id_ = self.msg_textview.connect('paste-clipboard',
            self._on_message_textview_paste_event)
        self.handlers[id_] = self.msg_textview
302 303 304 305 306 307
        id_ = self.msg_textview.connect('key_press_event',
            self._on_message_textview_key_press_event)
        self.handlers[id_] = self.msg_textview
        id_ = self.msg_textview.connect('populate_popup',
            self.on_msg_textview_populate_popup)
        self.handlers[id_] = self.msg_textview
308

309 310
        # Setup DND
        id_ = self.msg_textview.connect('drag_data_received',
311
                                        self._on_drag_data_received)
312
        self.handlers[id_] = self.msg_textview
313 314 315 316
        self.msg_textview.drag_dest_set(
            Gtk.DestDefaults.ALL,
            self.dnd_list,
            Gdk.DragAction.COPY)
317 318 319 320 321 322 323 324

        # the following vars are used to keep history of user's messages
        self.sent_history = []
        self.sent_history_pos = 0
        self.received_history = []
        self.received_history_pos = 0
        self.orig_msg = None

325 326 327
        # For XEP-0333
        self.last_msg_id = None

Philipp Hörist's avatar
Philipp Hörist committed
328
        self.set_emoticon_popover()
329 330

        # Attach speller
Philipp Hörist's avatar
Philipp Hörist committed
331
        self.set_speller()
332 333 334 335 336 337 338 339
        self.conv_textview.tv.show()

        # For XEP-0172
        self.user_nick = None

        self.command_hits = []
        self.last_key_tabs = False

340
        con = app.connections[self.account]
341
        con.get_module('Chatstate').set_active(self.contact)
342

343
        id_ = self.msg_textview.connect('text-changed',
344
            self._on_message_tv_buffer_changed)
345
        self.handlers[id_] = self.msg_textview
346 347 348 349 350
        if parent_win is not None:
            id_ = parent_win.window.connect('motion-notify-event',
                self._on_window_motion_notify)
            self.handlers[id_] = parent_win.window

351
        self.encryption = self.get_encryption_state()
352
        self.conv_textview.encryption_enabled = self.encryption is not None
Philipp Hörist's avatar
Philipp Hörist committed
353

354 355
        # PluginSystem: adding GUI extension point for ChatControlBase
        # instance object (also subclasses, eg. ChatControl or GroupchatControl)
356
        app.plugin_manager.gui_extension_point('chat_control_base', self)
357

358
        app.ged.register_event_handler('our-show', ged.GUI1,
359
            self._nec_our_status)
360
        app.ged.register_event_handler('ping-sent', ged.GUI1,
361
            self._nec_ping)
362
        app.ged.register_event_handler('ping-reply', ged.GUI1,
363
            self._nec_ping)
364
        app.ged.register_event_handler('ping-error', ged.GUI1,
365
            self._nec_ping)
Philipp Hörist's avatar
Philipp Hörist committed
366
        app.ged.register_event_handler('sec-catalog-received', ged.GUI1,
367
            self._sec_labels_received)
Philipp Hörist's avatar
Philipp Hörist committed
368 369
        app.ged.register_event_handler('style-changed', ged.GUI1,
            self._style_changed)
370

Alexander Krotov's avatar
Alexander Krotov committed
371
        # This is basically a very nasty hack to surpass the inability
372 373 374
        # to properly use the super, because of the old code.
        CommandTools.__init__(self)

375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
    def delegate_action(self, action):
        if action == 'browse-history':
            dict_ = {'jid': GLib.Variant('s', self.contact.jid),
                     'account': GLib.Variant('s', self.account)}
            variant = GLib.Variant('a{sv}', dict_)
            app.app.activate_action('browse-history', variant)
            return Gdk.EVENT_STOP

        if action == 'clear-chat':
            self.conv_textview.clear()
            return Gdk.EVENT_STOP

        if action == 'delete-line':
            self.clear(self.msg_textview)
            return Gdk.EVENT_STOP

        if action == 'show-emoji-chooser':
392
            self.xml.emoticons_button.get_popover().show()
393 394
            return Gdk.EVENT_STOP

395 396
        if action == 'copy-text':
            self.conv_textview.tv.emit('copy-clipboard')
Philipp Hörist's avatar
Philipp Hörist committed
397
            return Gdk.EVENT_STOP
398

399 400
        return Gdk.EVENT_PROPAGATE

401
    def add_actions(self):
Philipp Hörist's avatar
Philipp Hörist committed
402
        action = Gio.SimpleAction.new_stateful(
403
            "set-encryption-%s" % self.control_id,
Philipp Hörist's avatar
Philipp Hörist committed
404
            GLib.VariantType.new("s"),
405
            GLib.Variant("s", self.encryption or 'disabled'))
Philipp Hörist's avatar
Philipp Hörist committed
406
        action.connect("change-state", self.change_encryption)
Philipp Hörist's avatar
Philipp Hörist committed
407 408
        self.parent_win.window.add_action(action)

409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
        action = Gio.SimpleAction.new(
            'send-file-%s' % self.control_id, None)
        action.connect('activate', self._on_send_file)
        action.set_enabled(False)
        self.parent_win.window.add_action(action)

        action = Gio.SimpleAction.new(
            'send-file-httpupload-%s' % self.control_id, None)
        action.connect('activate', self._on_send_httpupload)
        action.set_enabled(False)
        self.parent_win.window.add_action(action)

        action = Gio.SimpleAction.new(
            'send-file-jingle-%s' % self.control_id, None)
        action.connect('activate', self._on_send_jingle)
        action.set_enabled(False)
        self.parent_win.window.add_action(action)

Philipp Hörist's avatar
Philipp Hörist committed
427 428
    # Actions

Philipp Hörist's avatar
Philipp Hörist committed
429
    def change_encryption(self, action, param):
Philipp Hörist's avatar
Philipp Hörist committed
430
        encryption = param.get_string()
431 432 433
        if encryption == 'disabled':
            encryption = None

Philipp Hörist's avatar
Philipp Hörist committed
434 435 436
        if self.encryption == encryption:
            return

437
        if encryption:
438
            plugin = app.plugin_manager.encryption_plugins[encryption]
Philipp Hörist's avatar
Philipp Hörist committed
439 440
            if not plugin.activate_encryption(self):
                return
Philipp Hörist's avatar
Philipp Hörist committed
441

Philipp Hörist's avatar
Philipp Hörist committed
442
        action.set_state(param)
443
        self.set_encryption_state(encryption)
444
        self.set_encryption_menu_icon()
Philipp Hörist's avatar
Philipp Hörist committed
445 446
        self.set_lock_image()

447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
    def set_lock_image(self):
        encryption_state = {'visible': self.encryption is not None,
                            'enc_type': self.encryption,
                            'authenticated': False}

        if self.encryption:
            app.plugin_manager.extension_point(
                'encryption_state' + self.encryption, self, encryption_state)

        visible, enc_type, authenticated = encryption_state.values()

        if authenticated:
            authenticated_string = _('and authenticated')
            self.xml.lock_image.set_from_icon_name(
                'security-high-symbolic', Gtk.IconSize.MENU)
        else:
            authenticated_string = _('and NOT authenticated')
            self.xml.lock_image.set_from_icon_name(
                'security-low-symbolic', Gtk.IconSize.MENU)

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

        self.xml.authentication_button.set_tooltip_text(tooltip)
        self.xml.authentication_button.set_visible(visible)
        self.xml.lock_image.set_sensitive(visible)

    def _on_authentication_button_clicked(self, _button):
        app.plugin_manager.extension_point(
            'encryption_dialog' + self.encryption, self)

478 479 480
    def set_encryption_state(self, encryption):
        config_key = '%s-%s' % (self.account, self.contact.jid)
        self.encryption = encryption
481
        self.conv_textview.encryption_enabled = encryption is not None
482
        app.config.set_per('encryption', config_key,
483
                           'encryption', self.encryption or '')
484 485 486

    def get_encryption_state(self):
        config_key = '%s-%s' % (self.account, self.contact.jid)
487
        state = app.config.get_per('encryption', config_key, 'encryption')
488 489
        if not state:
            return None
490
        if state not in app.plugin_manager.encryption_plugins:
491 492 493
            self.set_encryption_state(None)
            return None
        return state
Philipp Hörist's avatar
Philipp Hörist committed
494

495
    def set_encryption_menu_icon(self):
496
        image = self.xml.encryption_menu.get_image()
Philipp Hörist's avatar
Philipp Hörist committed
497 498
        if image is None:
            image = Gtk.Image()
499
            self.xml.encryption_menu.set_image(image)
500
        if not self.encryption:
Philipp Hörist's avatar
Philipp Hörist committed
501 502
            image.set_from_icon_name('channel-insecure-symbolic',
                                     Gtk.IconSize.MENU)
503
        else:
Philipp Hörist's avatar
Philipp Hörist committed
504 505
            image.set_from_icon_name('channel-secure-symbolic',
                                     Gtk.IconSize.MENU)
506

507
    def set_speller(self):
508
        if not app.is_installed('GSPELL') or not app.config.get('use_speller'):
Philipp Hörist's avatar
Philipp Hörist committed
509 510
            return

Philipp Hörist's avatar
Philipp Hörist committed
511 512 513
        gspell_lang = self.get_speller_language()
        if gspell_lang is None:
            return
Philipp Hörist's avatar
Philipp Hörist committed
514

515
        spell_checker = Gspell.Checker.new(gspell_lang)
Philipp Hörist's avatar
Philipp Hörist committed
516 517
        spell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
            self.msg_textview.get_buffer())
518
        spell_buffer.set_spell_checker(spell_checker)
Philipp Hörist's avatar
Philipp Hörist committed
519 520 521
        spell_view = Gspell.TextView.get_from_gtk_text_view(self.msg_textview)
        spell_view.set_inline_spell_checking(False)
        spell_view.set_enable_language_menu(True)
Philipp Hörist's avatar
Philipp Hörist committed
522

523
        spell_checker.connect('notify::language', self.on_language_changed)
Philipp Hörist's avatar
Philipp Hörist committed
524 525

    def get_speller_language(self):
526
        per_type = 'contacts'
Philipp Hörist's avatar
Philipp Hörist committed
527
        if self.type_id == 'gc':
528
            per_type = 'rooms'
Philipp Hörist's avatar
Philipp Hörist committed
529 530
        lang = app.config.get_per(
            per_type, self.contact.jid, 'speller_language')
531 532
        if not lang:
            # use the default one
533
            lang = app.config.get('speller_language')
534
            if not lang:
535
                lang = i18n.LANG
Philipp Hörist's avatar
Philipp Hörist committed
536 537 538 539
        gspell_lang = Gspell.language_lookup(lang)
        if gspell_lang is None:
            gspell_lang = Gspell.language_get_default()
        return gspell_lang
540

Philipp Hörist's avatar
Philipp Hörist committed
541 542
    def on_language_changed(self, checker, param):
        gspell_lang = checker.get_language()
543 544 545
        per_type = 'contacts'
        if self.type_id == message_control.TYPE_GC:
            per_type = 'rooms'
546 547
        if not app.config.get_per(per_type, self.contact.jid):
            app.config.add_per(per_type, self.contact.jid)
Philipp Hörist's avatar
Philipp Hörist committed
548 549
        app.config.set_per(per_type, self.contact.jid,
                           'speller_language', gspell_lang.get_code())
550

551 552 553 554 555 556 557
    def _on_html_textview_grab_focus(self, textview):
        # Abort signal so the textview does not get focused
        # Focus the MessageTextView instead
        GObject.signal_stop_emission_by_name(textview, 'grab-focus')
        self.msg_textview.grab_focus()
        return Gdk.EVENT_STOP

558 559
    def on_banner_label_populate_popup(self, label, menu):
        """
Alexander Krotov's avatar
Alexander Krotov committed
560
        Override the default context menu and add our own menuitems
561 562 563 564
        """
        item = Gtk.SeparatorMenuItem.new()
        menu.prepend(item)

565
        menu2 = self.prepare_context_menu()  # pylint: disable=assignment-from-none
566 567 568 569 570 571 572 573 574 575 576 577
        i = 0
        for item in menu2:
            menu2.remove(item)
            menu.prepend(item)
            menu.reorder_child(item, i)
            i += 1
        menu.show_all()

    def shutdown(self):
        super(ChatControlBase, self).shutdown()
        # PluginSystem: removing GUI extension points connected with ChatControlBase
        # instance object
Philipp Hörist's avatar
Philipp Hörist committed
578
        app.plugin_manager.remove_gui_extension_point('chat_control_base', self)
579
        app.plugin_manager.remove_gui_extension_point(
580
            'chat_control_base_draw_banner', self)
Philipp Hörist's avatar
Philipp Hörist committed
581 582
        app.plugin_manager.remove_gui_extension_point(
            'chat_control_base_update_toolbar', self)
583
        app.ged.remove_event_handler('our-show', ged.GUI1,
584
            self._nec_our_status)
Philipp Hörist's avatar
Philipp Hörist committed
585
        app.ged.remove_event_handler('sec-catalog-received', ged.GUI1,
586
            self._sec_labels_received)
Philipp Hörist's avatar
Philipp Hörist committed
587 588 589
        app.ged.remove_event_handler('style-changed', ged.GUI1,
            self._style_changed)

590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608

    def on_msg_textview_populate_popup(self, textview, menu):
        """
        Override the default context menu and we prepend an option to switch
        languages
        """
        item = Gtk.MenuItem.new_with_mnemonic(_('_Undo'))
        menu.prepend(item)
        id_ = item.connect('activate', self.msg_textview.undo)
        self.handlers[id_] = item

        item = Gtk.SeparatorMenuItem.new()
        menu.prepend(item)

        item = Gtk.MenuItem.new_with_mnemonic(_('_Clear'))
        menu.prepend(item)
        id_ = item.connect('activate', self.msg_textview.clear)
        self.handlers[id_] = item

609 610 611 612 613
        paste_item = Gtk.MenuItem.new_with_label(_('Paste as quote'))
        id_ = paste_item.connect('activate', self.paste_clipboard_as_quote)
        self.handlers[id_] = paste_item
        menu.append(paste_item)

614 615
        menu.show_all()

616
    def insert_as_quote(self, text: str) -> None:
Philipp Hörist's avatar
Philipp Hörist committed
617
        self.msg_textview.remove_placeholder()
618
        text = '> ' + text.replace('\n', '\n> ') + '\n'
619 620 621
        message_buffer = self.msg_textview.get_buffer()
        message_buffer.insert_at_cursor(text)

622 623 624 625 626 627 628 629
    def paste_clipboard_as_quote(self, _item: Gtk.MenuItem) -> None:
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        text = clipboard.wait_for_text()
        self.insert_as_quote(text)

    def on_quote(self, widget, text):
        self.insert_as_quote(text)

630 631 632 633 634 635 636 637
    # moved from ChatControl
    def _on_banner_eventbox_button_press_event(self, widget, event):
        """
        If right-clicked, show popup
        """
        if event.button == 3:  # right click
            self.parent_win.popup_menu(event)

638 639 640 641 642
    def _on_message_textview_paste_event(self, texview):
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        image = clipboard.wait_for_image()
        if image is not None:
            if not app.config.get('confirm_paste_image'):
643
                self._paste_event_confirmed(True, image)
644
                return
645 646
            NewConfirmationCheckDialog(
                _('Paste Image'),
647 648
                _('You are trying to paste an image'),
                _('Are you sure you want to paste your '
649 650
                  'clipboard\'s image into the chat window?'),
                _('_Do not ask me again'),
651
                [DialogButton.make('Cancel'),
652
                 DialogButton.make('Accept',
653
                                   text=_('_Paste'),
654
                                   is_default=True,
655 656
                                   callback=self._paste_event_confirmed,
                                   args=[image])]).show()
657

658 659 660
    def _paste_event_confirmed(self, is_checked, image):
        if is_checked:
            app.config.set('confirm_paste_image', False)
661 662 663
        tmp_dir = TemporaryDirectory()
        dir_ = tmp_dir.name

664
        # Get file transfer preference
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
        ft_pref = app.config.get_per('accounts', self.account,
                                     'filetransfer_preference')
        path = os.path.join(dir_, '0.png')
        image.savev(path, 'png', [], [])
        con = app.connections[self.account]
        win = self.parent_win.window
        httpupload = win.lookup_action(
            'send-file-httpupload-%s' % self.control_id)
        jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)

        if self.type_id == message_control.TYPE_GC:
            # groupchat only supports httpupload on drag and drop
            if httpupload.get_enabled():
                # use httpupload
                con.get_module('HTTPUpload').check_file_before_transfer(
                    path, self.encryption, self.contact,
                    self.session, groupchat=True)
        else:
            if httpupload.get_enabled() and jingle.get_enabled():
                if ft_pref == 'httpupload':
                    con.get_module('HTTPUpload').check_file_before_transfer(
                        path, self.encryption, self.contact, self.session)
                else:
                    ft = app.interface.instances['file_transfers']
                    ft.send_file(self.account, self.contact, path)
            elif httpupload.get_enabled():
                con.get_module('HTTPUpload').check_file_before_transfer(
                    path, self.encryption, self.contact, self.session)
            elif jingle.get_enabled():
                ft = app.interface.instances['file_transfers']
                ft.send_file(self.account, self.contact, path)

        tmp_dir.cleanup()

699 700 701 702 703 704 705
    def _on_message_textview_key_press_event(self, widget, event):
        if event.keyval == Gdk.KEY_space:
            self.space_pressed = True

        elif (self.space_pressed or self.msg_textview.undo_pressed) and \
        event.keyval not in (Gdk.KEY_Control_L, Gdk.KEY_Control_R) and \
        not (event.keyval == Gdk.KEY_z and event.get_state() & Gdk.ModifierType.CONTROL_MASK):
Alexander Krotov's avatar
Alexander Krotov committed
706 707 708
            # If the space key has been pressed and now it hasn't,
            # we save the buffer into the undo list. But be careful we're not
            # pressing Control again (as in ctrl+z)
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
            _buffer = widget.get_buffer()
            start_iter, end_iter = _buffer.get_bounds()
            self.msg_textview.save_undo(_buffer.get_text(start_iter, end_iter, True))
            self.space_pressed = False

        # Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here
        if self.widget_name == 'groupchat_control':
            if event.keyval not in (Gdk.KEY_ISO_Left_Tab, Gdk.KEY_Tab):
                self.last_key_tabs = False
        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
            # CTRL + SHIFT + TAB
            if event.get_state() & Gdk.ModifierType.CONTROL_MASK and \
                            event.keyval == Gdk.KEY_ISO_Left_Tab:
                self.parent_win.move_to_next_unread_tab(False)
                return True
            # SHIFT + PAGE_[UP|DOWN]: send to conv_textview
725
            if event.keyval == Gdk.KEY_Page_Down or \
726 727
                            event.keyval == Gdk.KEY_Page_Up:
                self.conv_textview.tv.event(event)
728
                self._on_scroll(None, event.keyval)
729
                return True
730
        if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
            if event.keyval == Gdk.KEY_Tab:  # CTRL + TAB
                self.parent_win.move_to_next_unread_tab(True)
                return True

        message_buffer = self.msg_textview.get_buffer()
        event_state = event.get_state()
        if event.keyval == Gdk.KEY_Tab:
            start, end = message_buffer.get_bounds()
            position = message_buffer.get_insert()
            end = message_buffer.get_iter_at_mark(position)
            text = message_buffer.get_text(start, end, False)
            splitted = text.split()
            if (text.startswith(self.COMMAND_PREFIX) and not
            text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1):
                text = splitted[0]
                bare = text.lstrip(self.COMMAND_PREFIX)
                if len(text) == 1:
                    self.command_hits = []
                    for command in self.list_commands():
                        for name in command.names:
                            self.command_hits.append(name)
                else:
                    if (self.last_key_tabs and self.command_hits and
                            self.command_hits[0].startswith(bare)):
                        self.command_hits.append(self.command_hits.pop(0))
                    else:
                        self.command_hits = []
                        for command in self.list_commands():
                            for name in command.names:
                                if name.startswith(bare):
                                    self.command_hits.append(name)

                if self.command_hits:
                    message_buffer.delete(start, end)
                    message_buffer.insert_at_cursor(self.COMMAND_PREFIX + \
                        self.command_hits[0] + ' ')
                    self.last_key_tabs = True
                return True
            if self.widget_name != 'groupchat_control':
                self.last_key_tabs = False
        if event.keyval == Gdk.KEY_Up:
            if event_state & Gdk.ModifierType.CONTROL_MASK:
                if event_state & Gdk.ModifierType.SHIFT_MASK: # Ctrl+Shift+UP
                    self.scroll_messages('up', message_buffer, 'received')
                else:  # Ctrl+UP
                    self.scroll_messages('up', message_buffer, 'sent')
777
                return True
778 779 780 781 782 783
        elif event.keyval == Gdk.KEY_Down:
            if event_state & Gdk.ModifierType.CONTROL_MASK:
                if event_state & Gdk.ModifierType.SHIFT_MASK: # Ctrl+Shift+Down
                    self.scroll_messages('down', message_buffer, 'received')
                else:  # Ctrl+Down
                    self.scroll_messages('down', message_buffer, 'sent')
784
                return True
785 786 787 788
        elif event.keyval == Gdk.KEY_Return or \
        event.keyval == Gdk.KEY_KP_Enter:  # ENTER
            message_textview = widget
            message_buffer = message_textview.get_buffer()
Philipp Hörist's avatar
Philipp Hörist committed
789
            message_textview.replace_emojis()
790 791 792 793
            start_iter, end_iter = message_buffer.get_bounds()
            message = message_buffer.get_text(start_iter, end_iter, False)
            xhtml = self.msg_textview.get_xhtml()

794 795 796 797
            if event_state & Gdk.ModifierType.SHIFT_MASK:
                send_message = False
            else:
                is_ctrl_enter = bool(event_state & Gdk.ModifierType.CONTROL_MASK)
798
                send_message = is_ctrl_enter == app.config.get('send_on_ctrl_enter')
799

800
            if send_message and not app.account_is_connected(self.account):
801
                # we are not connected
802
                app.interface.raise_dialog('not-connected-while-sending')
803 804 805 806
            elif send_message:
                self.send_message(message, xhtml=xhtml)
            else:
                message_buffer.insert_at_cursor('\n')
807 808 809
                mark = message_buffer.get_insert()
                iter_ = message_buffer.get_iter_at_mark(mark)
                if message_buffer.get_end_iter().equal(iter_):
Philipp Hörist's avatar
Philipp Hörist committed
810
                    GLib.idle_add(util.scroll_to_end, self.msg_scrolledwindow)
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825

            return True
        elif event.keyval == Gdk.KEY_z: # CTRL+z
            if event_state & Gdk.ModifierType.CONTROL_MASK:
                self.msg_textview.undo()
                return True

        return False

    def _on_drag_data_received(self, widget, context, x, y, selection,
                    target_type, timestamp):
        """
        Derived types SHOULD implement this
        """

826
    def _on_drag_leave(self, *args):
827 828 829 830
        # FIXME: DND on non editable TextView, find a better way
        self.drag_entered = False
        self.conv_textview.tv.set_editable(False)

831
    def _on_drag_motion(self, *args):
832 833 834 835 836 837
        # FIXME: DND on non editable TextView, find a better way
        if not self.drag_entered:
            # We drag new data over the TextView, make it editable to catch dnd
            self.drag_entered_conv = True
            self.conv_textview.tv.set_editable(True)

838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
    def drag_data_file_transfer(self, contact, selection, widget):
        # get file transfer preference
        ft_pref = app.config.get_per('accounts', self.account,
                                     'filetransfer_preference')
        win = self.parent_win.window
        con = app.connections[self.account]
        httpupload = win.lookup_action(
            'send-file-httpupload-%s' % self.control_id)
        jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)

        # we may have more than one file dropped
        uri_splitted = selection.get_uris()
        for uri in uri_splitted:
            path = helpers.get_file_path_from_dnd_dropped_uri(uri)
            if not os.path.isfile(path):  # is it a file?
                continue
            if self.type_id == message_control.TYPE_GC:
                # groupchat only supports httpupload on drag and drop
                if httpupload.get_enabled():
                    # use httpupload
Philipp Hörist's avatar
Philipp Hörist committed
858
                    con.get_module('HTTPUpload').check_file_before_transfer(
859 860 861 862 863
                        path, self.encryption, contact,
                        self.session, groupchat=True)
            else:
                if httpupload.get_enabled() and jingle.get_enabled():
                    if ft_pref == 'httpupload':
Philipp Hörist's avatar
Philipp Hörist committed
864
                        con.get_module('HTTPUpload').check_file_before_transfer(
865 866 867 868 869
                            path, self.encryption, contact, self.session)
                    else:
                        ft = app.interface.instances['file_transfers']
                        ft.send_file(self.account, contact, path)
                elif httpupload.get_enabled():
Philipp Hörist's avatar
Philipp Hörist committed
870
                    con.get_module('HTTPUpload').check_file_before_transfer(
871 872 873 874 875
                        path, self.encryption, contact, self.session)
                elif jingle.get_enabled():
                    ft = app.interface.instances['file_transfers']
                    ft.send_file(self.account, contact, path)

876
    def get_seclabel(self):
877
        idx = self.xml.label_selector.get_active()
Philipp Hörist's avatar
Philipp Hörist committed
878 879 880 881
        if idx == -1:
            return

        con = app.connections[self.account]
882 883 884 885
        jid = self.contact.jid
        if self.TYPE_ID == 'pm':
            jid = self.gc_contact.room_jid
        catalog = con.get_module('SecLabels').get_catalog(jid)
Philipp Hörist's avatar
Philipp Hörist committed
886 887 888
        labels, label_list, _ = catalog
        lname = label_list[idx]
        label = labels[lname]
889 890
        return label

Philipp Hörist's avatar
Philipp Hörist committed
891
    def send_message(self, message, type_='chat',
892
    resource=None, xhtml=None, process_commands=True, attention=False):
893 894 895 896 897 898 899 900 901 902 903 904
        """
        Send the given message to the active tab. Doesn't return None if error
        """
        if not message or message == '\n':
            return None

        if process_commands and self.process_as_command(message):
            return

        label = self.get_seclabel()

        if self.correcting and self.last_sent_msg:
905
            correct_id = self.last_sent_msg
906
        else:
907
            correct_id = None
908

909 910 911 912
        con = app.connections[self.account]
        chatstate = con.get_module('Chatstate').get_active_chatstate(
            self.contact)

Philipp Hörist's avatar
Philipp Hörist committed
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
        event = MessageOutgoingEvent(None,
                                     account=self.account,
                                     jid=self.contact.jid,
                                     message=message,
                                     type_=type_,
                                     chatstate=chatstate,
                                     resource=resource,
                                     user_nick=self.user_nick,
                                     label=label,
                                     control=self,
                                     attention=attention,
                                     correct_id=correct_id,
                                     automatic_message=False,
                                     encryption=self.encryption)
        event.additional_data.set_value('gajim', 'xhtml', xhtml)
        app.nec.push_outgoing_event(event)
929 930 931 932 933 934 935 936 937 938 939

        # Record the history of sent messages
        self.save_message(message, 'sent')

        # Be sure to send user nickname only once according to JEP-0172
        self.user_nick = None

        # Clear msg input
        message_buffer = self.msg_textview.get_buffer()
        message_buffer.set_text('') # clear message buffer (and tv of course)

940
    def _on_window_motion_notify(self, *args):
941 942 943
        """
        It gets called no matter if it is the active window or not
        """
944 945 946
        if not self.parent_win:
            # when a groupchat is minimized there is no parent window
            return
947
        if self.parent_win.get_active_jid() == self.contact.jid:
948 949
            # if window is the active one, set last interaction
            con = app.connections[self.account]
950 951
            con.get_module('Chatstate').set_mouse_activity(
                self.contact, self.msg_textview.has_text())
952

953 954 955 956 957
    def _on_message_tv_buffer_changed(self, textview, textbuffer):
        if textbuffer.get_char_count() and self.encryption:
            app.plugin_manager.extension_point(
                'typing' + self.encryption, self)

958 959
        con = app.connections[self.account]
        con.get_module('Chatstate').set_keyboard_activity(self.contact)
960
        if not textview.has_text():
961 962
            con.get_module('Chatstate').set_chatstate_delayed(self.contact,
                                                              Chatstate.ACTIVE)
Philipp Hörist's avatar
Philipp Hörist committed
963
            return
964 965
        con.get_module('Chatstate').set_chatstate(self.contact,
                                                  Chatstate.COMPOSING)
966

967 968 969 970 971 972 973 974 975
    def save_message(self, message, msg_type):
        # save the message, so user can scroll though the list with key up/down
        if msg_type == 'sent':
            history = self.sent_history
            pos = self.sent_history_pos
        else:
            history = self.received_history
            pos = self.received_history_pos
        size = len(history)
976
        scroll = pos != size
Alexander Krotov's avatar
Alexander Krotov committed
977
        # we don't want size of the buffer to grow indefinitely
978
        max_size = app.config.get('key_up_lines')
979
        for _i in range(size - max_size + 1):
980 981 982 983 984 985 986 987 988 989 990 991 992
            if pos == 0:
                break
            history.pop(0)
            pos -= 1
        history.append(message)
        if not scroll or msg_type == 'sent':
            pos = len(history)
        if msg_type == 'sent':
            self.sent_history_pos = pos
            self.orig_msg = None
        else:
            self.received_history_pos = pos

993 994
    def add_info_message(self, text):
        self.conv_textview.print_conversation_line(
Philipp Hörist's avatar
Philipp Hörist committed
995
            text, 'info', '', None, graphics=False)
996 997 998

    def add_status_message(self, text):
        self.conv_textview.print_conversation_line(
Philipp Hörist's avatar
Philipp Hörist committed
999
            text, 'status', '', None)
1000

1001
    def add_message(self, text, kind, name, tim,
Philipp Hörist's avatar
Philipp Hörist committed
1002 1003
                    other_tags_for_name=None, other_tags_for_time=None,
                    other_tags_for_text=None, restored=False, subject=None,
Philipp Hörist's avatar
Philipp Hörist committed
1004
                    old_kind=None,
Philipp Hörist's avatar
Philipp Hörist committed
1005 1006
                    displaymarking=None, msg_log_id=None,
                    message_id=None, correct_id=None, additional_data=None,
1007
                    marker=None, error=None):
1008 1009 1010 1011 1012 1013 1014 1015
        """
        Print 'chat' type messages
        correct_id = (message_id, correct_id)
        """
        jid = self.contact.jid
        full_jid = self.get_full_jid()
        textview = self.conv_textview
        end = False
Philipp Hörist's avatar
Philipp Hörist committed
1016
        if self.conv_textview.autoscroll or kind == 'outgoing':
1017 1018
            end = True

1019 1020 1021 1022 1023 1024 1025
        if other_tags_for_name is None:
            other_tags_for_name = []
        if other_tags_for_time is None:
            other_tags_for_time = []
        if other_tags_for_text is None:
            other_tags_for_text = []
        if additional_data is None:
1026
            additional_data = AdditionalDataDict()
1027

Philipp Hörist's avatar
Philipp Hörist committed
1028
        textview.print_conversation_line(text, kind, name, tim,
1029
            other_tags_for_name, other_tags_for_time, other_tags_for_text,
Philipp Hörist's avatar
Philipp Hörist committed
1030
            subject, old_kind,
1031
            displaymarking=displaymarking, message_id=message_id,
Philipp Hörist's avatar
Philipp Hörist committed
1032
            correct_id=correct_id, additional_data=additional_data,
1033
            marker=marker, error=error)
1034

1035
        if restored:
1036
            return
1037

1038 1039 1040
        if message_id:
            self.last_msg_id = message_id

1041 1042
        if kind == 'incoming':
            if not self.type_id == message_control.TYPE_GC or \
Philipp Hörist's avatar
Philipp Hörist committed
1043
            app.config.notify_for_muc(jid) or \
1044 1045 1046 1047
            'marked' in other_tags_for_text:
                # it's a normal message, or a muc message with want to be
                # notified about if quitting just after
                # other_tags_for_text == ['marked'] --> highlighted gc message
1048
                app.last_message_time[self.account][full_jid] = time.time()
1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062

        if kind in ('incoming', 'incoming_queue'):
            # Record the history of received messages
            self.save_message(text, 'received')

        if kind in ('incoming', 'incoming_queue', 'error'):
            gc_message = False
            if self.type_id == message_control.TYPE_GC:
                gc_message = True

            if ((self.parent_win and (not self.parent_win.get_active_control() or \
            self != self.parent_win.get_active_control() or \
            not self.parent_win.is_active() or not end)) or \
            (gc_message and \
1063
            jid in app.interface.minimized_controls[self.account])) and \
1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
            kind in ('incoming', 'incoming_queue', 'error'):
                # we want to have save this message in events list
                # other_tags_for_text == ['marked'] --> highlighted gc message
                if gc_message:
                    if 'marked' in other_tags_for_text:
                        event_type = events.PrintedMarkedGcMsgEvent
                    else:
                        event_type = events.PrintedGcMsgEvent
                    event = 'gc_message_received'
                else:
                    if self.type_id == message_control.TYPE_CHAT:
                        event_type = events.PrintedChatEvent
                    else:
                        event_type = events.PrintedPmEvent
                    event = 'message_received'
Philipp Hörist's avatar
Philipp Hörist committed
1079 1080 1081
                show_in_roster = get_show_in_roster(event, self.session)
                show_in_systray = get_show_in_systray(
                    event_type.type_, self.contact.jid)
1082 1083

                event = event_type(text, subject, self, msg_log_id,
1084
                    message_id=message_id,
1085 1086
                    show_in_roster=show_in_roster,
                    show_in_systray=show_in_systray)
1087
                app.events.add_event(self.account, full_jid, event)
1088 1089
                # We need to redraw contact if we show in roster
                if show_in_roster:
1090
                    app.interface.roster.draw_contact(self.contact.jid,
1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
                        self.account)

        if not self.parent_win:
            return

        if (not self.parent_win.get_active_control() or \
        self != self.parent_win.get_active_control() or \
        not self.parent_win.is_active() or not end) and \
        kind in ('incoming', 'incoming_queue', 'error'):
            self.parent_win.redraw_tab(self)
            if not self.parent_win.is_active():
                self.parent_win.show_title(True, self) # Enabled Urgent hint
            else:
                self.parent_win.show_title(False, self) # Disabled Urgent hint

    def toggle_emoticons(self):
        """
Philipp Hörist's avatar
Philipp Hörist committed
1108
        Hide show emoticons_button
1109
        """
1110
        if app.config.get('emoticons_theme'):
1111 1112
            self.xml.emoticons_button.set_no_show_all(False)
            self.xml.emoticons_button.show()
1113
        else:
1114 1115
            self.xml.emoticons_button.set_no_show_all(True)
            self.xml.emoticons_button.hide()
Philipp Hörist's avatar
Philipp Hörist committed
1116 1117

    def set_emoticon_popover(self):
1118
        if not app.config.get('emoticons_theme'):
Philipp Hörist's avatar
Philipp Hörist committed
1119 1120 1121 1122 1123
            return

        if not self.parent_win:
            return

Philipp Hörist's avatar
Philipp Hörist committed
1124
        emoji_chooser.text_widget = self.msg_textview
1125
        self.xml.emoticons_button.set_popover(emoji_chooser)
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145

    def on_color_menuitem_activate(self, widget):
        color_dialog = Gtk.ColorChooserDialog(None, self.parent_win.window)
        color_dialog.set_use_alpha(False)
        color_dialog.connect('response', self.msg_textview.color_set)
        color_dialog.show_all()

    def on_font_menuitem_activate(self, widget):
        font_dialog = Gtk.FontChooserDialog(None, self.parent_win.window)
        start, finish = self.msg_textview.get_active_iters()
        font_dialog.connect('response', self.msg_textview.font_set, start, finish)
        font_dialog.show_all()

    def on_formatting_menuitem_activate(self, widget):
        tag = widget.get_name()
        self.msg_textview.set_tag(tag)

    def on_clear_formatting_menuitem_activate(self, widget):
        self.msg_textview.clear_tags()

Philipp Hörist's avatar
Philipp Hörist committed
1146 1147 1148
    def _style_changed(self, *args):
        self.update_tags()

1149 1150 1151 1152 1153 1154 1155 1156
    def update_tags(self):
        self.conv_textview.update_tags()

    def clear(self, tv):
        buffer_ = tv.get_buffer()
        start, end = buffer_.get_bounds()
        buffer_.delete(start, end)

1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183
    def _on_send_file(self, action, param):
        # get file transfer preference
        ft_pref = app.config.get_per('accounts', self.account,
                                     'filetransfer_preference')

        win = self.parent_win.window
        httpupload = win.lookup_action(
            'send-file-httpupload-%s' % self.control_id)
        jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)

        if httpupload.get_enabled() and jingle.get_enabled():
            if ft_pref == 'httpupload':
                httpupload.activate()
            else:
                jingle.activate()
        elif httpupload.get_enabled():
            httpupload.activate()
        elif jingle.get_enabled():
            jingle.activate()

    def _on_send_httpupload(self, action, param):
        app.interface.send_httpupload(self)

    def _on_send_jingle(self, action, param):
        self._on_send_file_jingle()

    def _on_send_file_jingle(self, gc_contact=None):
1184 1185 1186 1187
        """
        gc_contact can be set when we are in a groupchat control
        """
        def _on_ok(c):
1188
            app.interface.instances['file_transfers'].show_file_send_request(
1189
                self.account, c)
1190
        if self.type_id == message_control.TYPE_PM:
1191
            gc_contact = self.gc_contact
1192 1193 1194

        if not gc_contact:
            _on_ok(self.contact)
1195
            return
1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211

        # gc or pm
        gc_control = app.interface.msg_win_mgr.get_gc_control(
            gc_contact.room_jid, self.account)
        self_contact = app.contacts.get_gc_contact(self.account,
                                                   gc_control.room_jid,
                                                   gc_control.nick)
        if (gc_control.is_anonymous and
                gc_contact.affiliation.value not in ['admin', 'owner'] and
                self_contact.affiliation.value in ['admin', 'owner']):
            contact = app.contacts.get_contact(self.account, gc_contact.jid)
            if not contact or contact.sub not in ('both', 'to'):

                NewConfirmationDialog(
                    _('Privacy'),
                    _('Warning'),
1212 1213
                    _('If you send a file to <b>%s</b>, your real XMPP '
                      'address will be revealed.' % gc_contact.name),
1214 1215 1216
                    [DialogButton.make('Cancel'),
                     DialogButton.make(
                         'OK',
1217
                         text=_('_Continue'),
1218 1219 1220
                         callback=lambda: _on_ok(gc_contact))]).show()
                return
        _on_ok(gc_contact)
Emmanuel Gil Peyrot's avatar