1616 lines
61 KiB
Python
1616 lines
61 KiB
Python
|
# 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/>.
|
|||
|
|
|||
|
import os
|
|||
|
import sys
|
|||
|
import time
|
|||
|
import uuid
|
|||
|
import tempfile
|
|||
|
|
|||
|
from gi.repository import Gtk
|
|||
|
from gi.repository import Gdk
|
|||
|
from gi.repository import GLib
|
|||
|
from gi.repository import Gio
|
|||
|
|
|||
|
from gajim.common import events
|
|||
|
from gajim.common import app
|
|||
|
from gajim.common import helpers
|
|||
|
from gajim.common import ged
|
|||
|
from gajim.common import i18n
|
|||
|
from gajim.common.i18n import _
|
|||
|
from gajim.common.nec import EventHelper
|
|||
|
from gajim.common.helpers import AdditionalDataDict
|
|||
|
from gajim.common.helpers import event_filter
|
|||
|
from gajim.common.contacts import GC_Contact
|
|||
|
from gajim.common.const import Chatstate
|
|||
|
from gajim.common.structs import OutgoingMessage
|
|||
|
|
|||
|
from gajim import gtkgui_helpers
|
|||
|
|
|||
|
from gajim.conversation_textview import ConversationTextview
|
|||
|
|
|||
|
from gajim.gui.dialogs import DialogButton
|
|||
|
from gajim.gui.dialogs import ConfirmationDialog
|
|||
|
from gajim.gui.dialogs import PastePreviewDialog
|
|||
|
from gajim.gui.message_input import MessageInputTextView
|
|||
|
from gajim.gui.util import at_the_end
|
|||
|
from gajim.gui.util import get_show_in_roster
|
|||
|
from gajim.gui.util import get_show_in_systray
|
|||
|
from gajim.gui.util import get_hardware_key_codes
|
|||
|
from gajim.gui.util import get_builder
|
|||
|
from gajim.gui.util import generate_account_badge
|
|||
|
from gajim.gui.const import ControlType # pylint: disable=unused-import
|
|||
|
from gajim.gui.emoji_chooser import emoji_chooser
|
|||
|
|
|||
|
from gajim.command_system.implementation.middleware import ChatCommandProcessor
|
|||
|
from gajim.command_system.implementation.middleware import CommandTools
|
|||
|
|
|||
|
# 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.
|
|||
|
# pylint: disable=unused-import
|
|||
|
from gajim.command_system.implementation import standard
|
|||
|
from gajim.command_system.implementation import execute
|
|||
|
# pylint: enable=unused-import
|
|||
|
|
|||
|
if app.is_installed('GSPELL'):
|
|||
|
from gi.repository import Gspell # pylint: disable=ungrouped-imports
|
|||
|
|
|||
|
# 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_KEY_C = get_hardware_key_codes(Gdk.KEY_c)
|
|||
|
|
|||
|
if sys.platform == 'darwin':
|
|||
|
COPY_MODIFIER = Gdk.ModifierType.META_MASK
|
|||
|
COPY_MODIFIER_KEYS = (Gdk.KEY_Meta_L, Gdk.KEY_Meta_R)
|
|||
|
else:
|
|||
|
COPY_MODIFIER = Gdk.ModifierType.CONTROL_MASK
|
|||
|
COPY_MODIFIER_KEYS = (Gdk.KEY_Control_L, Gdk.KEY_Control_R)
|
|||
|
|
|||
|
|
|||
|
################################################################################
|
|||
|
class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
|
|||
|
"""
|
|||
|
A base class containing a banner, ConversationTextview, MessageInputTextView
|
|||
|
"""
|
|||
|
|
|||
|
_type = None # type: ControlType
|
|||
|
|
|||
|
def __init__(self, parent_win, widget_name, contact, acct,
|
|||
|
resource=None):
|
|||
|
EventHelper.__init__(self)
|
|||
|
# 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.
|
|||
|
_contact = app.contacts.get_contact_with_highest_priority(
|
|||
|
acct, contact.jid)
|
|||
|
if _contact and not isinstance(_contact, GC_Contact):
|
|||
|
contact = _contact
|
|||
|
|
|||
|
self.handlers = {}
|
|||
|
self.parent_win = parent_win
|
|||
|
self.contact = contact
|
|||
|
self.account = acct
|
|||
|
self.resource = resource
|
|||
|
|
|||
|
# control_id is a unique id for the control,
|
|||
|
# its used as action name for actions that belong to a control
|
|||
|
self.control_id = str(uuid.uuid4())
|
|||
|
self.session = None
|
|||
|
|
|||
|
app.last_message_time[self.account][self.get_full_jid()] = 0
|
|||
|
|
|||
|
self.xml = get_builder('%s.ui' % widget_name)
|
|||
|
self.xml.connect_signals(self)
|
|||
|
self.widget = self.xml.get_object('%s_hbox' % widget_name)
|
|||
|
|
|||
|
self._accounts = app.get_enabled_accounts_with_labels()
|
|||
|
if len(self._accounts) > 1:
|
|||
|
account_badge = generate_account_badge(self.account)
|
|||
|
account_badge.set_tooltip_text(
|
|||
|
_('Account: %s') % app.get_account_label(self.account))
|
|||
|
self.xml.account_badge.add(account_badge)
|
|||
|
account_badge.show()
|
|||
|
|
|||
|
# Drag and drop
|
|||
|
self.xml.overlay.add_overlay(self.xml.drop_area)
|
|||
|
self.xml.drop_area.hide()
|
|||
|
self.xml.overlay.connect(
|
|||
|
'drag-data-received', self._on_drag_data_received)
|
|||
|
self.xml.overlay.connect('drag-motion', self._on_drag_motion)
|
|||
|
self.xml.overlay.connect('drag-leave', self._on_drag_leave)
|
|||
|
|
|||
|
self.TARGET_TYPE_URI_LIST = 80
|
|||
|
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)]
|
|||
|
|
|||
|
self.xml.overlay.drag_dest_set(
|
|||
|
Gtk.DestDefaults.ALL,
|
|||
|
self._dnd_list,
|
|||
|
Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
|
|||
|
self.xml.overlay.drag_dest_set_target_list(dst_targets)
|
|||
|
|
|||
|
# 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
|
|||
|
|
|||
|
self.conv_textview.tv.connect('key-press-event',
|
|||
|
self._on_conv_textview_key_press_event)
|
|||
|
|
|||
|
# This is a workaround: as soon as a line break occurs in Gtk.TextView
|
|||
|
# with word-char wrapping enabled, a hyphen character is automatically
|
|||
|
# inserted before the line break. This triggers the hscrollbar to show,
|
|||
|
# see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2384
|
|||
|
# Using set_hscroll_policy(Gtk.Scrollable.Policy.NEVER) would cause bad
|
|||
|
# performance during resize, and prevent the window from being shrunk
|
|||
|
# horizontally under certain conditions (applies to GroupchatControl)
|
|||
|
hscrollbar = self.xml.conversation_scrolledwindow.get_hscrollbar()
|
|||
|
hscrollbar.hide()
|
|||
|
|
|||
|
self.xml.conversation_scrolledwindow.add(self.conv_textview.tv)
|
|||
|
widget = self.xml.conversation_scrolledwindow.get_vadjustment()
|
|||
|
widget.connect('changed', self.on_conversation_vadjustment_changed)
|
|||
|
|
|||
|
vscrollbar = self.xml.conversation_scrolledwindow.get_vscrollbar()
|
|||
|
vscrollbar.connect('button-release-event',
|
|||
|
self._on_scrollbar_button_release)
|
|||
|
|
|||
|
self.msg_textview = MessageInputTextView()
|
|||
|
self.msg_textview.connect('paste-clipboard',
|
|||
|
self._on_message_textview_paste_event)
|
|||
|
self.msg_textview.connect('key-press-event',
|
|||
|
self._on_message_textview_key_press_event)
|
|||
|
self.msg_textview.connect('populate-popup',
|
|||
|
self.on_msg_textview_populate_popup)
|
|||
|
self.msg_textview.get_buffer().connect(
|
|||
|
'changed', self._on_message_tv_buffer_changed)
|
|||
|
|
|||
|
# Send message button
|
|||
|
self.xml.send_message_button.set_action_name(
|
|||
|
'win.send-message-%s' % self.control_id)
|
|||
|
self.xml.send_message_button.set_visible(
|
|||
|
app.settings.get('show_send_message_button'))
|
|||
|
app.settings.bind_signal(
|
|||
|
'show_send_message_button',
|
|||
|
self.xml.send_message_button,
|
|||
|
'set_visible')
|
|||
|
|
|||
|
self.msg_scrolledwindow = ScrolledWindow()
|
|||
|
self.msg_scrolledwindow.set_margin_start(3)
|
|||
|
self.msg_scrolledwindow.set_margin_end(3)
|
|||
|
self.msg_scrolledwindow.get_style_context().add_class(
|
|||
|
'message-input-border')
|
|||
|
self.msg_scrolledwindow.add(self.msg_textview)
|
|||
|
|
|||
|
self.xml.hbox.pack_start(self.msg_scrolledwindow, True, True, 0)
|
|||
|
|
|||
|
# 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
|
|||
|
|
|||
|
# For XEP-0333
|
|||
|
self.last_msg_id = None
|
|||
|
|
|||
|
self.correcting = False
|
|||
|
self.last_sent_msg = None
|
|||
|
|
|||
|
self.set_emoticon_popover()
|
|||
|
|
|||
|
# Attach speller
|
|||
|
self.set_speller()
|
|||
|
self.conv_textview.tv.show()
|
|||
|
|
|||
|
# For XEP-0172
|
|||
|
self.user_nick = None
|
|||
|
|
|||
|
self.command_hits = []
|
|||
|
self.last_key_tabs = False
|
|||
|
|
|||
|
self.sendmessage = True
|
|||
|
|
|||
|
con = app.connections[self.account]
|
|||
|
con.get_module('Chatstate').set_active(self.contact)
|
|||
|
|
|||
|
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
|
|||
|
|
|||
|
self.encryption = self.get_encryption_state()
|
|||
|
self.conv_textview.encryption_enabled = self.encryption is not None
|
|||
|
|
|||
|
# PluginSystem: adding GUI extension point for ChatControlBase
|
|||
|
# instance object (also subclasses, eg. ChatControl or GroupchatControl)
|
|||
|
app.plugin_manager.gui_extension_point('chat_control_base', self)
|
|||
|
|
|||
|
# pylint: disable=line-too-long
|
|||
|
self.register_events([
|
|||
|
('our-show', ged.GUI1, self._nec_our_status),
|
|||
|
('ping-sent', ged.GUI1, self._nec_ping),
|
|||
|
('ping-reply', ged.GUI1, self._nec_ping),
|
|||
|
('ping-error', ged.GUI1, self._nec_ping),
|
|||
|
('sec-catalog-received', ged.GUI1, self._sec_labels_received),
|
|||
|
('style-changed', ged.GUI1, self._style_changed),
|
|||
|
])
|
|||
|
# pylint: enable=line-too-long
|
|||
|
|
|||
|
# This is basically a very nasty hack to surpass the inability
|
|||
|
# to properly use the super, because of the old code.
|
|||
|
CommandTools.__init__(self)
|
|||
|
|
|||
|
def _on_conv_textview_key_press_event(self, textview, event):
|
|||
|
if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
|
|||
|
if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
|
|||
|
return Gdk.EVENT_PROPAGATE
|
|||
|
|
|||
|
if event.keyval in COPY_MODIFIER_KEYS:
|
|||
|
# Don’t route modifier keys for copy action to the Message Input
|
|||
|
# otherwise pressing CTRL/META + c (the next event after that)
|
|||
|
# will not reach the textview (because the Message Input would get
|
|||
|
# focused).
|
|||
|
return Gdk.EVENT_PROPAGATE
|
|||
|
|
|||
|
if event.get_state() & COPY_MODIFIER:
|
|||
|
# Don’t reroute the event if it is META + c and the
|
|||
|
# textview has a selection
|
|||
|
if event.hardware_keycode in KEYCODES_KEY_C:
|
|||
|
if textview.get_buffer().props.has_selection:
|
|||
|
return Gdk.EVENT_PROPAGATE
|
|||
|
|
|||
|
if not self.msg_textview.get_sensitive():
|
|||
|
# If the input textview is not sensitive it can’t get the focus.
|
|||
|
# In that case propagate_key_event() would send the event again
|
|||
|
# to the conversation textview. This would mean a recursion.
|
|||
|
return Gdk.EVENT_PROPAGATE
|
|||
|
|
|||
|
# Focus the Message Input and resend the event
|
|||
|
self.msg_textview.grab_focus()
|
|||
|
self.msg_textview.get_toplevel().propagate_key_event(event)
|
|||
|
return Gdk.EVENT_STOP
|
|||
|
|
|||
|
@property
|
|||
|
def type(self):
|
|||
|
return self._type
|
|||
|
|
|||
|
@property
|
|||
|
def is_chat(self):
|
|||
|
return self._type.is_chat
|
|||
|
|
|||
|
@property
|
|||
|
def is_privatechat(self):
|
|||
|
return self._type.is_privatechat
|
|||
|
|
|||
|
@property
|
|||
|
def is_groupchat(self):
|
|||
|
return self._type.is_groupchat
|
|||
|
|
|||
|
def get_full_jid(self):
|
|||
|
fjid = self.contact.jid
|
|||
|
if self.resource:
|
|||
|
fjid += '/' + self.resource
|
|||
|
return fjid
|
|||
|
|
|||
|
def minimizable(self):
|
|||
|
"""
|
|||
|
Called to check if control can be minimized
|
|||
|
|
|||
|
Derived classes MAY implement this.
|
|||
|
"""
|
|||
|
return False
|
|||
|
|
|||
|
def safe_shutdown(self):
|
|||
|
"""
|
|||
|
Called to check if control can be closed without losing data.
|
|||
|
returns True if control can be closed safely else False
|
|||
|
|
|||
|
Derived classes MAY implement this.
|
|||
|
"""
|
|||
|
return True
|
|||
|
|
|||
|
def allow_shutdown(self, method, on_response_yes, on_response_no,
|
|||
|
on_response_minimize):
|
|||
|
"""
|
|||
|
Called to check is a control is allowed to shutdown.
|
|||
|
If a control is not in a suitable shutdown state this method
|
|||
|
should call on_response_no, else on_response_yes or
|
|||
|
on_response_minimize
|
|||
|
|
|||
|
Derived classes MAY implement this.
|
|||
|
"""
|
|||
|
on_response_yes(self)
|
|||
|
|
|||
|
def focus(self):
|
|||
|
raise NotImplementedError
|
|||
|
|
|||
|
def get_nb_unread(self):
|
|||
|
jid = self.contact.jid
|
|||
|
if self.resource:
|
|||
|
jid += '/' + self.resource
|
|||
|
return len(app.events.get_events(
|
|||
|
self.account,
|
|||
|
jid,
|
|||
|
['printed_%s' % self._type, str(self._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()
|
|||
|
|
|||
|
def update_toolbar(self):
|
|||
|
"""
|
|||
|
update state of buttons in toolbar
|
|||
|
"""
|
|||
|
self._update_toolbar()
|
|||
|
app.plugin_manager.gui_extension_point(
|
|||
|
'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_toolbar(self):
|
|||
|
"""
|
|||
|
Derived types MAY implement this
|
|||
|
"""
|
|||
|
|
|||
|
def get_tab_label(self, chatstate):
|
|||
|
"""
|
|||
|
Return a suitable tab label string. Returns a tuple such as: (label_str,
|
|||
|
color) either of which can be None if chatstate is given that means we
|
|||
|
have HE SENT US a chatstate and we want it displayed
|
|||
|
|
|||
|
Derivded classes MUST implement this.
|
|||
|
"""
|
|||
|
# Return a markup'd label and optional Gtk.Color in a tuple like:
|
|||
|
# return (label_str, None)
|
|||
|
|
|||
|
def get_tab_image(self):
|
|||
|
# Return a suitable tab image for display.
|
|||
|
return None
|
|||
|
|
|||
|
def prepare_context_menu(self, hide_buttonbar_items=False):
|
|||
|
"""
|
|||
|
Derived classes SHOULD implement this
|
|||
|
"""
|
|||
|
return None
|
|||
|
|
|||
|
def set_session(self, session):
|
|||
|
oldsession = None
|
|||
|
if hasattr(self, 'session'):
|
|||
|
oldsession = self.session
|
|||
|
|
|||
|
if oldsession and session == oldsession:
|
|||
|
return
|
|||
|
|
|||
|
self.session = session
|
|||
|
|
|||
|
if session:
|
|||
|
session.control = self
|
|||
|
|
|||
|
if session and oldsession:
|
|||
|
oldsession.control = None
|
|||
|
|
|||
|
def remove_session(self, session):
|
|||
|
if session != self.session:
|
|||
|
return
|
|||
|
self.session.control = None
|
|||
|
self.session = None
|
|||
|
|
|||
|
@event_filter(['account'])
|
|||
|
def _nec_our_status(self, event):
|
|||
|
if event.show == 'connecting':
|
|||
|
return
|
|||
|
|
|||
|
if event.show == 'offline':
|
|||
|
self.got_disconnected()
|
|||
|
else:
|
|||
|
self.got_connected()
|
|||
|
if self.parent_win:
|
|||
|
self.parent_win.redraw_tab(self)
|
|||
|
|
|||
|
def _nec_ping(self, obj):
|
|||
|
raise NotImplementedError
|
|||
|
|
|||
|
def setup_seclabel(self):
|
|||
|
self.xml.label_selector.hide()
|
|||
|
self.xml.label_selector.set_no_show_all(True)
|
|||
|
lb = Gtk.ListStore(str)
|
|||
|
self.xml.label_selector.set_model(lb)
|
|||
|
cell = Gtk.CellRendererText()
|
|||
|
cell.set_property('xpad', 5) # padding for status text
|
|||
|
self.xml.label_selector.pack_start(cell, True)
|
|||
|
# text to show is in in first column of liststore
|
|||
|
self.xml.label_selector.add_attribute(cell, 'text', 0)
|
|||
|
con = app.connections[self.account]
|
|||
|
jid = self.contact.jid
|
|||
|
if self._type.is_privatechat:
|
|||
|
jid = self.gc_contact.room_jid
|
|||
|
if con.get_module('SecLabels').supported:
|
|||
|
con.get_module('SecLabels').request_catalog(jid)
|
|||
|
|
|||
|
def _sec_labels_received(self, event):
|
|||
|
if event.account != self.account:
|
|||
|
return
|
|||
|
|
|||
|
jid = self.contact.jid
|
|||
|
if self._type.is_privatechat:
|
|||
|
jid = self.gc_contact.room_jid
|
|||
|
|
|||
|
if event.jid != jid:
|
|||
|
return
|
|||
|
model = self.xml.label_selector.get_model()
|
|||
|
model.clear()
|
|||
|
|
|||
|
sel = 0
|
|||
|
labellist = event.catalog.get_label_names()
|
|||
|
default = event.catalog.default
|
|||
|
for index, label in enumerate(labellist):
|
|||
|
model.append([label])
|
|||
|
if label == default:
|
|||
|
sel = index
|
|||
|
|
|||
|
self.xml.label_selector.set_active(sel)
|
|||
|
self.xml.label_selector.set_no_show_all(False)
|
|||
|
self.xml.label_selector.show_all()
|
|||
|
|
|||
|
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':
|
|||
|
if sys.platform in ('win32', 'darwin'):
|
|||
|
self.xml.emoticons_button.get_popover().show()
|
|||
|
return Gdk.EVENT_STOP
|
|||
|
self.msg_textview.emit('insert-emoji')
|
|||
|
return Gdk.EVENT_STOP
|
|||
|
|
|||
|
return Gdk.EVENT_PROPAGATE
|
|||
|
|
|||
|
def add_actions(self):
|
|||
|
action = Gio.SimpleAction.new_stateful(
|
|||
|
'set-encryption-%s' % self.control_id,
|
|||
|
GLib.VariantType.new('s'),
|
|||
|
GLib.Variant('s', self.encryption or 'disabled'))
|
|||
|
action.connect('change-state', self.change_encryption)
|
|||
|
self.parent_win.window.add_action(action)
|
|||
|
|
|||
|
actions = {
|
|||
|
'send-message-%s': self._on_send_message,
|
|||
|
'send-file-%s': self._on_send_file,
|
|||
|
'send-file-httpupload-%s': self._on_send_file,
|
|||
|
'send-file-jingle-%s': self._on_send_file,
|
|||
|
}
|
|||
|
|
|||
|
for name, func in actions.items():
|
|||
|
action = Gio.SimpleAction.new(name % self.control_id, None)
|
|||
|
action.connect('activate', func)
|
|||
|
action.set_enabled(False)
|
|||
|
self.parent_win.window.add_action(action)
|
|||
|
|
|||
|
def remove_actions(self):
|
|||
|
actions = [
|
|||
|
'send-message-',
|
|||
|
'set-encryption-',
|
|||
|
'send-file-',
|
|||
|
'send-file-httpupload-',
|
|||
|
'send-file-jingle-',
|
|||
|
]
|
|||
|
|
|||
|
for action in actions:
|
|||
|
self.parent_win.window.remove_action(f'{action}{self.control_id}')
|
|||
|
|
|||
|
def change_encryption(self, action, param):
|
|||
|
encryption = param.get_string()
|
|||
|
if encryption == 'disabled':
|
|||
|
encryption = None
|
|||
|
|
|||
|
if self.encryption == encryption:
|
|||
|
return
|
|||
|
|
|||
|
if encryption:
|
|||
|
plugin = app.plugin_manager.encryption_plugins[encryption]
|
|||
|
if not plugin.activate_encryption(self):
|
|||
|
return
|
|||
|
|
|||
|
action.set_state(param)
|
|||
|
self.set_encryption_state(encryption)
|
|||
|
self.set_encryption_menu_icon()
|
|||
|
self.set_lock_image()
|
|||
|
|
|||
|
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)
|
|||
|
|
|||
|
def set_encryption_state(self, encryption):
|
|||
|
self.encryption = encryption
|
|||
|
self.conv_textview.encryption_enabled = encryption is not None
|
|||
|
self.contact.settings.set('encryption', self.encryption or '')
|
|||
|
|
|||
|
def get_encryption_state(self):
|
|||
|
state = self.contact.settings.get('encryption')
|
|||
|
if not state:
|
|||
|
return None
|
|||
|
if state not in app.plugin_manager.encryption_plugins:
|
|||
|
self.set_encryption_state(None)
|
|||
|
return None
|
|||
|
return state
|
|||
|
|
|||
|
def set_encryption_menu_icon(self):
|
|||
|
image = self.xml.encryption_menu.get_image()
|
|||
|
if image is None:
|
|||
|
image = Gtk.Image()
|
|||
|
self.xml.encryption_menu.set_image(image)
|
|||
|
if not self.encryption:
|
|||
|
image.set_from_icon_name('channel-insecure-symbolic',
|
|||
|
Gtk.IconSize.MENU)
|
|||
|
else:
|
|||
|
image.set_from_icon_name('channel-secure-symbolic',
|
|||
|
Gtk.IconSize.MENU)
|
|||
|
|
|||
|
def set_speller(self):
|
|||
|
if not app.is_installed('GSPELL') or not app.settings.get('use_speller'):
|
|||
|
return
|
|||
|
|
|||
|
gspell_lang = self.get_speller_language()
|
|||
|
spell_checker = Gspell.Checker.new(gspell_lang)
|
|||
|
spell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
|
|||
|
self.msg_textview.get_buffer())
|
|||
|
spell_buffer.set_spell_checker(spell_checker)
|
|||
|
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)
|
|||
|
|
|||
|
spell_checker.connect('notify::language', self.on_language_changed)
|
|||
|
|
|||
|
def get_speller_language(self):
|
|||
|
lang = self.contact.settings.get('speller_language')
|
|||
|
if not lang:
|
|||
|
# use the default one
|
|||
|
lang = app.settings.get('speller_language')
|
|||
|
if not lang:
|
|||
|
lang = i18n.LANG
|
|||
|
gspell_lang = Gspell.language_lookup(lang)
|
|||
|
if gspell_lang is None:
|
|||
|
gspell_lang = Gspell.language_get_default()
|
|||
|
return gspell_lang
|
|||
|
|
|||
|
def on_language_changed(self, checker, _param):
|
|||
|
gspell_lang = checker.get_language()
|
|||
|
self.contact.settings.set('speller_language', gspell_lang.get_code())
|
|||
|
|
|||
|
def on_banner_label_populate_popup(self, _label, menu):
|
|||
|
"""
|
|||
|
Override the default context menu and add our own menuitems
|
|||
|
"""
|
|||
|
item = Gtk.SeparatorMenuItem.new()
|
|||
|
menu.prepend(item)
|
|||
|
|
|||
|
menu2 = self.prepare_context_menu() # pylint: disable=assignment-from-none
|
|||
|
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):
|
|||
|
# remove_gui_extension_point() is called on shutdown, but also when
|
|||
|
# a plugin is getting disabled. Plugins don’t know the difference.
|
|||
|
# Plugins might want to remove their widgets on
|
|||
|
# remove_gui_extension_point(), so delete the objects only afterwards.
|
|||
|
app.plugin_manager.remove_gui_extension_point('chat_control_base', self)
|
|||
|
app.plugin_manager.remove_gui_extension_point(
|
|||
|
'chat_control_base_update_toolbar', self)
|
|||
|
|
|||
|
for i in list(self.handlers.keys()):
|
|||
|
if self.handlers[i].handler_is_connected(i):
|
|||
|
self.handlers[i].disconnect(i)
|
|||
|
self.handlers.clear()
|
|||
|
|
|||
|
self.conv_textview.del_handlers()
|
|||
|
del self.conv_textview
|
|||
|
del self.msg_textview
|
|||
|
del self.msg_scrolledwindow
|
|||
|
|
|||
|
self.widget.destroy()
|
|||
|
del self.widget
|
|||
|
|
|||
|
del self.xml
|
|||
|
|
|||
|
self.unregister_events()
|
|||
|
|
|||
|
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
|
|||
|
|
|||
|
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)
|
|||
|
|
|||
|
menu.show_all()
|
|||
|
|
|||
|
def insert_as_quote(self, text: str) -> None:
|
|||
|
text = '> ' + text.replace('\n', '\n> ') + '\n'
|
|||
|
message_buffer = self.msg_textview.get_buffer()
|
|||
|
message_buffer.insert_at_cursor(text)
|
|||
|
|
|||
|
def paste_clipboard_as_quote(self, _item: Gtk.MenuItem) -> None:
|
|||
|
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|||
|
text = clipboard.wait_for_text()
|
|||
|
if text is None:
|
|||
|
return
|
|||
|
self.insert_as_quote(text)
|
|||
|
|
|||
|
def on_quote(self, _widget, text):
|
|||
|
self.insert_as_quote(text)
|
|||
|
|
|||
|
# 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)
|
|||
|
|
|||
|
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.settings.get('confirm_paste_image'):
|
|||
|
self._paste_event_confirmed(True, image)
|
|||
|
return
|
|||
|
PastePreviewDialog(
|
|||
|
_('Paste Image'),
|
|||
|
_('You are trying to paste an image'),
|
|||
|
_('Are you sure you want to paste your '
|
|||
|
'clipboard\'s image into the chat window?'),
|
|||
|
_('_Do not ask me again'),
|
|||
|
image,
|
|||
|
[DialogButton.make('Cancel'),
|
|||
|
DialogButton.make('Accept',
|
|||
|
text=_('_Paste'),
|
|||
|
callback=self._paste_event_confirmed,
|
|||
|
args=[image])]).show()
|
|||
|
|
|||
|
def _paste_event_confirmed(self, is_checked, image):
|
|||
|
if is_checked:
|
|||
|
app.settings.set('confirm_paste_image', False)
|
|||
|
|
|||
|
dir_ = tempfile.gettempdir()
|
|||
|
path = os.path.join(dir_, '%s.png' % str(uuid.uuid4()))
|
|||
|
image.savev(path, 'png', [], [])
|
|||
|
|
|||
|
self._start_filetransfer(path)
|
|||
|
|
|||
|
def _get_pref_ft_method(self):
|
|||
|
ft_pref = app.settings.get_account_setting(self.account,
|
|||
|
'filetransfer_preference')
|
|||
|
httpupload = self.parent_win.window.lookup_action(
|
|||
|
'send-file-httpupload-%s' % self.control_id)
|
|||
|
jingle = self.parent_win.window.lookup_action(
|
|||
|
'send-file-jingle-%s' % self.control_id)
|
|||
|
|
|||
|
if self._type.is_groupchat:
|
|||
|
if httpupload.get_enabled():
|
|||
|
return 'httpupload'
|
|||
|
return None
|
|||
|
|
|||
|
if httpupload.get_enabled() and jingle.get_enabled():
|
|||
|
return ft_pref
|
|||
|
|
|||
|
if httpupload.get_enabled():
|
|||
|
return 'httpupload'
|
|||
|
|
|||
|
if jingle.get_enabled():
|
|||
|
return 'jingle'
|
|||
|
return None
|
|||
|
|
|||
|
def _start_filetransfer(self, path):
|
|||
|
method = self._get_pref_ft_method()
|
|||
|
if method is None:
|
|||
|
return
|
|||
|
|
|||
|
if method == 'httpupload':
|
|||
|
app.interface.send_httpupload(self, path)
|
|||
|
|
|||
|
else:
|
|||
|
ft = app.interface.instances['file_transfers']
|
|||
|
ft.send_file(self.account, self.contact, path)
|
|||
|
|
|||
|
def _on_message_textview_key_press_event(self, textview, 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):
|
|||
|
# 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)
|
|||
|
_buffer = textview.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._type.is_groupchat:
|
|||
|
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:
|
|||
|
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
|
|||
|
|
|||
|
if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
|
|||
|
self.conv_textview.tv.event(event)
|
|||
|
self._on_scroll(None, event.keyval)
|
|||
|
return True
|
|||
|
|
|||
|
if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
|||
|
if event.keyval == Gdk.KEY_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)
|
|||
|
split = text.split()
|
|||
|
if (text.startswith(self.COMMAND_PREFIX) and
|
|||
|
not text.startswith(self.COMMAND_PREFIX * 2) and
|
|||
|
len(split) == 1):
|
|||
|
text = split[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 not self._type.is_groupchat:
|
|||
|
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')
|
|||
|
return True
|
|||
|
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')
|
|||
|
return True
|
|||
|
elif (event.keyval == Gdk.KEY_Return or
|
|||
|
event.keyval == Gdk.KEY_KP_Enter): # ENTER
|
|||
|
|
|||
|
if event_state & Gdk.ModifierType.SHIFT_MASK:
|
|||
|
textview.insert_newline()
|
|||
|
return True
|
|||
|
|
|||
|
if event_state & Gdk.ModifierType.CONTROL_MASK:
|
|||
|
if not app.settings.get('send_on_ctrl_enter'):
|
|||
|
textview.insert_newline()
|
|||
|
return True
|
|||
|
else:
|
|||
|
if app.settings.get('send_on_ctrl_enter'):
|
|||
|
textview.insert_newline()
|
|||
|
return True
|
|||
|
|
|||
|
if not app.account_is_available(self.account):
|
|||
|
# we are not connected
|
|||
|
app.interface.raise_dialog('not-connected-while-sending')
|
|||
|
return True
|
|||
|
|
|||
|
self._on_send_message()
|
|||
|
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
|
|||
|
"""
|
|||
|
|
|||
|
def _on_drag_leave(self, *args):
|
|||
|
self.xml.drop_area.set_no_show_all(True)
|
|||
|
self.xml.drop_area.hide()
|
|||
|
|
|||
|
def _on_drag_motion(self, *args):
|
|||
|
self.xml.drop_area.set_no_show_all(False)
|
|||
|
self.xml.drop_area.show_all()
|
|||
|
|
|||
|
def drag_data_file_transfer(self, selection):
|
|||
|
# 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
|
|||
|
|
|||
|
self._start_filetransfer(path)
|
|||
|
|
|||
|
def get_seclabel(self):
|
|||
|
idx = self.xml.label_selector.get_active()
|
|||
|
if idx == -1:
|
|||
|
return None
|
|||
|
|
|||
|
con = app.connections[self.account]
|
|||
|
jid = self.contact.jid
|
|||
|
if self._type.is_privatechat:
|
|||
|
jid = self.gc_contact.room_jid
|
|||
|
catalog = con.get_module('SecLabels').get_catalog(jid)
|
|||
|
labels, label_list = catalog.labels, catalog.get_label_names()
|
|||
|
lname = label_list[idx]
|
|||
|
label = labels[lname]
|
|||
|
return label
|
|||
|
|
|||
|
def _on_send_message(self, *args):
|
|||
|
self.msg_textview.replace_emojis()
|
|||
|
message = self.msg_textview.get_text()
|
|||
|
xhtml = self.msg_textview.get_xhtml()
|
|||
|
self.send_message(message, xhtml=xhtml)
|
|||
|
|
|||
|
def send_message(self,
|
|||
|
message,
|
|||
|
type_='chat',
|
|||
|
resource=None,
|
|||
|
xhtml=None,
|
|||
|
process_commands=True,
|
|||
|
attention=False):
|
|||
|
"""
|
|||
|
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:
|
|||
|
correct_id = self.last_sent_msg
|
|||
|
else:
|
|||
|
correct_id = None
|
|||
|
|
|||
|
con = app.connections[self.account]
|
|||
|
chatstate = con.get_module('Chatstate').get_active_chatstate(
|
|||
|
self.contact)
|
|||
|
|
|||
|
message_ = OutgoingMessage(account=self.account,
|
|||
|
contact=self.contact,
|
|||
|
message=message,
|
|||
|
type_=type_,
|
|||
|
chatstate=chatstate,
|
|||
|
resource=resource,
|
|||
|
user_nick=self.user_nick,
|
|||
|
label=label,
|
|||
|
control=self,
|
|||
|
attention=attention,
|
|||
|
correct_id=correct_id,
|
|||
|
xhtml=xhtml)
|
|||
|
|
|||
|
con.send_message(message_)
|
|||
|
|
|||
|
# 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)
|
|||
|
|
|||
|
def _on_window_motion_notify(self, *args):
|
|||
|
"""
|
|||
|
It gets called no matter if it is the active window or not
|
|||
|
"""
|
|||
|
if not self.parent_win:
|
|||
|
# when a groupchat is minimized there is no parent window
|
|||
|
return
|
|||
|
if self.parent_win.get_active_jid() == self.contact.jid:
|
|||
|
# if window is the active one, set last interaction
|
|||
|
con = app.connections[self.account]
|
|||
|
con.get_module('Chatstate').set_mouse_activity(
|
|||
|
self.contact, self.msg_textview.has_text())
|
|||
|
|
|||
|
def _on_message_tv_buffer_changed(self, textbuffer):
|
|||
|
has_text = self.msg_textview.has_text()
|
|||
|
if self.parent_win is not None:
|
|||
|
self.parent_win.window.lookup_action(
|
|||
|
'send-message-' + self.control_id).set_enabled(has_text)
|
|||
|
|
|||
|
if textbuffer.get_char_count() and self.encryption:
|
|||
|
app.plugin_manager.extension_point(
|
|||
|
'typing' + self.encryption, self)
|
|||
|
|
|||
|
con = app.connections[self.account]
|
|||
|
con.get_module('Chatstate').set_keyboard_activity(self.contact)
|
|||
|
if not has_text:
|
|||
|
con.get_module('Chatstate').set_chatstate_delayed(self.contact,
|
|||
|
Chatstate.ACTIVE)
|
|||
|
return
|
|||
|
con.get_module('Chatstate').set_chatstate(self.contact,
|
|||
|
Chatstate.COMPOSING)
|
|||
|
|
|||
|
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)
|
|||
|
scroll = pos != size
|
|||
|
# we don't want size of the buffer to grow indefinitely
|
|||
|
max_size = app.settings.get('key_up_lines')
|
|||
|
for _i in range(size - max_size + 1):
|
|||
|
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
|
|||
|
|
|||
|
def add_info_message(self, text, message_id=None):
|
|||
|
self.conv_textview.print_conversation_line(
|
|||
|
text, 'info', '', None, message_id=message_id, graphics=False)
|
|||
|
|
|||
|
def add_status_message(self, text):
|
|||
|
self.conv_textview.print_conversation_line(
|
|||
|
text, 'status', '', None)
|
|||
|
|
|||
|
def add_message(self,
|
|||
|
text,
|
|||
|
kind,
|
|||
|
name,
|
|||
|
tim,
|
|||
|
other_tags_for_name=None,
|
|||
|
other_tags_for_time=None,
|
|||
|
other_tags_for_text=None,
|
|||
|
restored=False,
|
|||
|
subject=None,
|
|||
|
old_kind=None,
|
|||
|
displaymarking=None,
|
|||
|
msg_log_id=None,
|
|||
|
message_id=None,
|
|||
|
stanza_id=None,
|
|||
|
correct_id=None,
|
|||
|
additional_data=None,
|
|||
|
marker=None,
|
|||
|
error=None):
|
|||
|
"""
|
|||
|
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
|
|||
|
if self.conv_textview.autoscroll or kind == 'outgoing':
|
|||
|
end = True
|
|||
|
|
|||
|
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:
|
|||
|
additional_data = AdditionalDataDict()
|
|||
|
|
|||
|
textview.print_conversation_line(text,
|
|||
|
kind,
|
|||
|
name,
|
|||
|
tim,
|
|||
|
other_tags_for_name,
|
|||
|
other_tags_for_time,
|
|||
|
other_tags_for_text,
|
|||
|
subject,
|
|||
|
old_kind,
|
|||
|
displaymarking=displaymarking,
|
|||
|
message_id=message_id,
|
|||
|
correct_id=correct_id,
|
|||
|
additional_data=additional_data,
|
|||
|
marker=marker,
|
|||
|
error=error)
|
|||
|
|
|||
|
if restored:
|
|||
|
return
|
|||
|
|
|||
|
if message_id:
|
|||
|
if self._type.is_groupchat:
|
|||
|
self.last_msg_id = stanza_id or message_id
|
|||
|
else:
|
|||
|
self.last_msg_id = message_id
|
|||
|
|
|||
|
if kind == 'incoming':
|
|||
|
if (not self._type.is_groupchat or
|
|||
|
self.contact.can_notify() or
|
|||
|
'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
|
|||
|
app.last_message_time[self.account][full_jid] = time.time()
|
|||
|
|
|||
|
if kind in ('incoming', 'incoming_queue'):
|
|||
|
# Record the history of received messages
|
|||
|
self.save_message(text, 'received')
|
|||
|
|
|||
|
# Send chat marker if we’re actively following the chat
|
|||
|
if self.parent_win and self.contact.settings.get('send_marker'):
|
|||
|
if (self.parent_win.get_active_control() == self and
|
|||
|
self.parent_win.is_active() and
|
|||
|
self.has_focus() and end):
|
|||
|
con = app.connections[self.account]
|
|||
|
con.get_module('ChatMarkers').send_displayed_marker(
|
|||
|
self.contact,
|
|||
|
self.last_msg_id,
|
|||
|
self._type)
|
|||
|
|
|||
|
if kind in ('incoming', 'incoming_queue', 'error'):
|
|||
|
gc_message = False
|
|||
|
if self._type.is_groupchat:
|
|||
|
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 \
|
|||
|
jid in app.interface.minimized_controls[self.account])) and \
|
|||
|
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.is_chat:
|
|||
|
event_type = events.PrintedChatEvent
|
|||
|
else:
|
|||
|
event_type = events.PrintedPmEvent
|
|||
|
event = 'message_received'
|
|||
|
show_in_roster = get_show_in_roster(event, self.session)
|
|||
|
show_in_systray = get_show_in_systray(
|
|||
|
event_type.type_, self.account, self.contact.jid)
|
|||
|
|
|||
|
event = event_type(text,
|
|||
|
subject,
|
|||
|
self,
|
|||
|
msg_log_id,
|
|||
|
message_id=message_id,
|
|||
|
stanza_id=stanza_id,
|
|||
|
show_in_roster=show_in_roster,
|
|||
|
show_in_systray=show_in_systray)
|
|||
|
app.events.add_event(self.account, full_jid, event)
|
|||
|
# We need to redraw contact if we show in roster
|
|||
|
if show_in_roster:
|
|||
|
app.interface.roster.draw_contact(self.contact.jid,
|
|||
|
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):
|
|||
|
"""
|
|||
|
Hide show emoticons_button
|
|||
|
"""
|
|||
|
if app.settings.get('emoticons_theme'):
|
|||
|
self.xml.emoticons_button.set_no_show_all(False)
|
|||
|
self.xml.emoticons_button.show()
|
|||
|
else:
|
|||
|
self.xml.emoticons_button.set_no_show_all(True)
|
|||
|
self.xml.emoticons_button.hide()
|
|||
|
|
|||
|
def set_emoticon_popover(self):
|
|||
|
if not app.settings.get('emoticons_theme'):
|
|||
|
return
|
|||
|
|
|||
|
if not self.parent_win:
|
|||
|
return
|
|||
|
|
|||
|
if sys.platform in ('win32', 'darwin'):
|
|||
|
emoji_chooser.text_widget = self.msg_textview
|
|||
|
self.xml.emoticons_button.set_popover(emoji_chooser)
|
|||
|
return
|
|||
|
|
|||
|
self.xml.emoticons_button.set_sensitive(True)
|
|||
|
self.xml.emoticons_button.connect('clicked',
|
|||
|
self._on_emoticon_button_clicked)
|
|||
|
|
|||
|
def _on_emoticon_button_clicked(self, _widget):
|
|||
|
# Present GTK emoji chooser (not cross platform compatible)
|
|||
|
self.msg_textview.emit('insert-emoji')
|
|||
|
self.xml.emoticons_button.set_property('active', False)
|
|||
|
|
|||
|
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()
|
|||
|
|
|||
|
def _style_changed(self, *args):
|
|||
|
self.update_tags()
|
|||
|
|
|||
|
def update_tags(self):
|
|||
|
self.conv_textview.update_tags()
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def clear(tv):
|
|||
|
buffer_ = tv.get_buffer()
|
|||
|
start, end = buffer_.get_bounds()
|
|||
|
buffer_.delete(start, end)
|
|||
|
|
|||
|
def _on_send_file(self, action, _param):
|
|||
|
name = action.get_name()
|
|||
|
if 'httpupload' in name:
|
|||
|
app.interface.send_httpupload(self)
|
|||
|
return
|
|||
|
|
|||
|
if 'jingle' in name:
|
|||
|
self._on_send_file_jingle()
|
|||
|
return
|
|||
|
|
|||
|
method = self._get_pref_ft_method()
|
|||
|
if method is None:
|
|||
|
return
|
|||
|
|
|||
|
if method == 'httpupload':
|
|||
|
app.interface.send_httpupload(self)
|
|||
|
else:
|
|||
|
self._on_send_file_jingle()
|
|||
|
|
|||
|
def _on_send_file_jingle(self, gc_contact=None):
|
|||
|
"""
|
|||
|
gc_contact can be set when we are in a groupchat control
|
|||
|
"""
|
|||
|
def _on_ok(_contact):
|
|||
|
app.interface.instances['file_transfers'].show_file_send_request(
|
|||
|
self.account, _contact)
|
|||
|
|
|||
|
if self._type.is_privatechat:
|
|||
|
gc_contact = self.gc_contact
|
|||
|
|
|||
|
if not gc_contact:
|
|||
|
_on_ok(self.contact)
|
|||
|
return
|
|||
|
|
|||
|
# 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'):
|
|||
|
|
|||
|
ConfirmationDialog(
|
|||
|
_('Privacy'),
|
|||
|
_('Warning'),
|
|||
|
_('If you send a file to <b>%s</b>, your real XMPP '
|
|||
|
'address will be revealed.') % gc_contact.name,
|
|||
|
[DialogButton.make('Cancel'),
|
|||
|
DialogButton.make(
|
|||
|
'OK',
|
|||
|
text=_('_Continue'),
|
|||
|
callback=lambda: _on_ok(gc_contact))]).show()
|
|||
|
return
|
|||
|
_on_ok(gc_contact)
|
|||
|
|
|||
|
def set_control_active(self, state):
|
|||
|
con = app.connections[self.account]
|
|||
|
if state:
|
|||
|
self.set_emoticon_popover()
|
|||
|
jid = self.contact.jid
|
|||
|
if self.conv_textview.autoscroll:
|
|||
|
# we are at the end
|
|||
|
type_ = [f'printed_{self._type}']
|
|||
|
if self._type.is_groupchat:
|
|||
|
type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
|
|||
|
if not app.events.remove_events(self.account,
|
|||
|
self.get_full_jid(),
|
|||
|
types=type_):
|
|||
|
# There were events to remove
|
|||
|
self.redraw_after_event_removed(jid)
|
|||
|
# XEP-0333 Send <displayed> marker
|
|||
|
con.get_module('ChatMarkers').send_displayed_marker(
|
|||
|
self.contact,
|
|||
|
self.last_msg_id,
|
|||
|
self._type)
|
|||
|
self.last_msg_id = None
|
|||
|
# send chatstate inactive to the one we're leaving
|
|||
|
# and active to the one we visit
|
|||
|
if self.msg_textview.has_text():
|
|||
|
con.get_module('Chatstate').set_chatstate(self.contact,
|
|||
|
Chatstate.PAUSED)
|
|||
|
else:
|
|||
|
con.get_module('Chatstate').set_chatstate(self.contact,
|
|||
|
Chatstate.ACTIVE)
|
|||
|
else:
|
|||
|
con.get_module('Chatstate').set_chatstate(self.contact,
|
|||
|
Chatstate.INACTIVE)
|
|||
|
|
|||
|
def scroll_to_end(self, force=False):
|
|||
|
self.conv_textview.scroll_to_end(force)
|
|||
|
|
|||
|
def _on_edge_reached(self, _scrolledwindow, pos):
|
|||
|
if pos != Gtk.PositionType.BOTTOM:
|
|||
|
return
|
|||
|
# Remove all events and set autoscroll True
|
|||
|
app.log('autoscroll').info('Autoscroll enabled')
|
|||
|
self.conv_textview.autoscroll = True
|
|||
|
if self.resource:
|
|||
|
jid = self.contact.get_full_jid()
|
|||
|
else:
|
|||
|
jid = self.contact.jid
|
|||
|
types_list = []
|
|||
|
if self._type.is_groupchat:
|
|||
|
types_list = ['printed_gc_msg', 'gc_msg', 'printed_marked_gc_msg']
|
|||
|
else:
|
|||
|
types_list = [f'printed_{self._type}', str(self._type)]
|
|||
|
|
|||
|
if not app.events.get_events(self.account, jid, types_list):
|
|||
|
return
|
|||
|
if not self.parent_win:
|
|||
|
return
|
|||
|
if (self.parent_win.get_active_control() == self and
|
|||
|
self.parent_win.window.is_active()):
|
|||
|
# we are at the end
|
|||
|
if not app.events.remove_events(
|
|||
|
self.account, jid, types=types_list):
|
|||
|
# There were events to remove
|
|||
|
self.redraw_after_event_removed(jid)
|
|||
|
# XEP-0333 Send <displayed> tag
|
|||
|
con = app.connections[self.account]
|
|||
|
con.get_module('ChatMarkers').send_displayed_marker(
|
|||
|
self.contact,
|
|||
|
self.last_msg_id,
|
|||
|
self._type)
|
|||
|
self.last_msg_id = None
|
|||
|
|
|||
|
def _on_scrollbar_button_release(self, scrollbar, event):
|
|||
|
if event.get_button()[1] != 1:
|
|||
|
# We want only to catch the left mouse button
|
|||
|
return
|
|||
|
if not at_the_end(scrollbar.get_parent()):
|
|||
|
app.log('autoscroll').info('Autoscroll disabled')
|
|||
|
self.conv_textview.autoscroll = False
|
|||
|
|
|||
|
def has_focus(self):
|
|||
|
if self.parent_win:
|
|||
|
if self.parent_win.window.get_property('has-toplevel-focus'):
|
|||
|
if self == self.parent_win.get_active_control():
|
|||
|
return True
|
|||
|
return False
|
|||
|
|
|||
|
def _on_scroll(self, widget, event):
|
|||
|
if not self.conv_textview.autoscroll:
|
|||
|
# autoscroll is already disabled
|
|||
|
return
|
|||
|
|
|||
|
if widget is None:
|
|||
|
# call from _conv_textview_key_press_event()
|
|||
|
# SHIFT + Gdk.KEY_Page_Up
|
|||
|
if event != Gdk.KEY_Page_Up:
|
|||
|
return
|
|||
|
else:
|
|||
|
# On scrolling UP disable autoscroll
|
|||
|
# get_scroll_direction() sets has_direction only TRUE
|
|||
|
# if smooth scrolling is deactivated. If we have smooth
|
|||
|
# smooth scrolling we have to use get_scroll_deltas()
|
|||
|
has_direction, direction = event.get_scroll_direction()
|
|||
|
if not has_direction:
|
|||
|
direction = None
|
|||
|
smooth, delta_x, delta_y = event.get_scroll_deltas()
|
|||
|
if smooth:
|
|||
|
if delta_y < 0:
|
|||
|
direction = Gdk.ScrollDirection.UP
|
|||
|
elif delta_y > 0:
|
|||
|
direction = Gdk.ScrollDirection.DOWN
|
|||
|
elif delta_x < 0:
|
|||
|
direction = Gdk.ScrollDirection.LEFT
|
|||
|
elif delta_x > 0:
|
|||
|
direction = Gdk.ScrollDirection.RIGHT
|
|||
|
else:
|
|||
|
app.log('autoscroll').warning(
|
|||
|
'Scroll directions can’t be determined')
|
|||
|
|
|||
|
if direction != Gdk.ScrollDirection.UP:
|
|||
|
return
|
|||
|
# Check if we have a Scrollbar
|
|||
|
adjustment = self.xml.conversation_scrolledwindow.get_vadjustment()
|
|||
|
if adjustment.get_upper() != adjustment.get_page_size():
|
|||
|
app.log('autoscroll').info('Autoscroll disabled')
|
|||
|
self.conv_textview.autoscroll = False
|
|||
|
|
|||
|
def on_conversation_vadjustment_changed(self, _adjustment):
|
|||
|
self.scroll_to_end()
|
|||
|
|
|||
|
def redraw_after_event_removed(self, jid):
|
|||
|
"""
|
|||
|
We just removed a 'printed_*' event, redraw contact in roster or
|
|||
|
gc_roster and titles in roster and msg_win
|
|||
|
"""
|
|||
|
if not self.parent_win: # minimized groupchat
|
|||
|
return
|
|||
|
self.parent_win.redraw_tab(self)
|
|||
|
self.parent_win.show_title()
|
|||
|
# TODO : get the contact and check get_show_in_roster()
|
|||
|
if self._type.is_privatechat:
|
|||
|
room_jid, nick = app.get_room_and_nick_from_fjid(jid)
|
|||
|
groupchat_control = app.interface.msg_win_mgr.get_gc_control(
|
|||
|
room_jid, self.account)
|
|||
|
if room_jid in app.interface.minimized_controls[self.account]:
|
|||
|
groupchat_control = \
|
|||
|
app.interface.minimized_controls[self.account][room_jid]
|
|||
|
contact = app.contacts.get_contact_with_highest_priority(
|
|||
|
self.account, room_jid)
|
|||
|
if contact:
|
|||
|
app.interface.roster.draw_contact(room_jid, self.account)
|
|||
|
if groupchat_control:
|
|||
|
groupchat_control.roster.draw_contact(nick)
|
|||
|
if groupchat_control.parent_win:
|
|||
|
groupchat_control.parent_win.redraw_tab(groupchat_control)
|
|||
|
else:
|
|||
|
app.interface.roster.draw_contact(jid, self.account)
|
|||
|
app.interface.roster.show_title()
|
|||
|
|
|||
|
def scroll_messages(self, direction, msg_buf, msg_type):
|
|||
|
if msg_type == 'sent':
|
|||
|
history = self.sent_history
|
|||
|
pos = self.sent_history_pos
|
|||
|
self.received_history_pos = len(self.received_history)
|
|||
|
else:
|
|||
|
history = self.received_history
|
|||
|
pos = self.received_history_pos
|
|||
|
self.sent_history_pos = len(self.sent_history)
|
|||
|
size = len(history)
|
|||
|
if self.orig_msg is None:
|
|||
|
# user was typing something and then went into history, so save
|
|||
|
# whatever is already typed
|
|||
|
start_iter = msg_buf.get_start_iter()
|
|||
|
end_iter = msg_buf.get_end_iter()
|
|||
|
self.orig_msg = msg_buf.get_text(start_iter, end_iter, False)
|
|||
|
if pos == size and size > 0 and direction == 'up' and \
|
|||
|
msg_type == 'sent' and not self.correcting and (not \
|
|||
|
history[pos - 1].startswith('/') or history[pos - 1].startswith('/me')):
|
|||
|
self.correcting = True
|
|||
|
gtkgui_helpers.add_css_class(
|
|||
|
self.msg_textview, 'gajim-msg-correcting')
|
|||
|
message = history[pos - 1]
|
|||
|
msg_buf.set_text(message)
|
|||
|
return
|
|||
|
if self.correcting:
|
|||
|
# We were previously correcting
|
|||
|
gtkgui_helpers.remove_css_class(
|
|||
|
self.msg_textview, 'gajim-msg-correcting')
|
|||
|
self.correcting = False
|
|||
|
pos += -1 if direction == 'up' else +1
|
|||
|
if pos == -1:
|
|||
|
return
|
|||
|
if pos >= size:
|
|||
|
pos = size
|
|||
|
message = self.orig_msg
|
|||
|
self.orig_msg = None
|
|||
|
else:
|
|||
|
message = history[pos]
|
|||
|
if msg_type == 'sent':
|
|||
|
self.sent_history_pos = pos
|
|||
|
else:
|
|||
|
self.received_history_pos = pos
|
|||
|
if self.orig_msg is not None:
|
|||
|
message = '> %s\n' % message.replace('\n', '\n> ')
|
|||
|
msg_buf.set_text(message)
|
|||
|
|
|||
|
def got_connected(self):
|
|||
|
self.msg_textview.set_sensitive(True)
|
|||
|
self.msg_textview.set_editable(True)
|
|||
|
self.update_toolbar()
|
|||
|
|
|||
|
def got_disconnected(self):
|
|||
|
self.msg_textview.set_sensitive(False)
|
|||
|
self.msg_textview.set_editable(False)
|
|||
|
self.conv_textview.tv.grab_focus()
|
|||
|
|
|||
|
self.update_toolbar()
|
|||
|
|
|||
|
|
|||
|
class ScrolledWindow(Gtk.ScrolledWindow):
|
|||
|
def __init__(self, *args, **kwargs):
|
|||
|
Gtk.ScrolledWindow.__init__(self, *args, **kwargs)
|
|||
|
|
|||
|
self.set_overlay_scrolling(False)
|
|||
|
self.set_max_content_height(100)
|
|||
|
self.set_propagate_natural_height(True)
|
|||
|
self.get_style_context().add_class('scrolled-no-border')
|
|||
|
self.get_style_context().add_class('no-scroll-indicator')
|
|||
|
self.get_style_context().add_class('scrollbar-style')
|
|||
|
self.get_style_context().add_class('one-line-scrollbar')
|
|||
|
self.set_shadow_type(Gtk.ShadowType.IN)
|
|||
|
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|