On some Desktops eg. Mate, the MessageTextview was to big, because the default height seems to be 82. What influences the default height (compare Windows where it is 46), is unclear.
1397 lines
56 KiB
Python
1397 lines
56 KiB
Python
# -*- coding:utf-8 -*-
|
|
## src/chat_control_base.py
|
|
##
|
|
## 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 time
|
|
from gi.repository import Gtk
|
|
from gi.repository import Gdk
|
|
from gi.repository import Pango
|
|
from gi.repository import GLib
|
|
from gi.repository import Gio
|
|
|
|
from gajim import gtkgui_helpers
|
|
from gajim.gtkgui_helpers import Color
|
|
from gajim import message_control
|
|
from gajim import dialogs
|
|
from gajim import history_window
|
|
from gajim import notify
|
|
import re
|
|
|
|
from gajim import emoticons
|
|
from gajim.common import events
|
|
from gajim.common import app
|
|
from gajim.common import helpers
|
|
from gajim.common import ged
|
|
from gajim.message_control import MessageControl
|
|
from gajim.conversation_textview import ConversationTextview
|
|
from gajim.message_textview import MessageTextView
|
|
from gajim.common.contacts import GC_Contact
|
|
from gajim.common.connection_handlers_events import MessageOutgoingEvent
|
|
|
|
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.
|
|
from gajim.command_system.implementation import standard
|
|
from gajim.command_system.implementation import execute
|
|
|
|
if app.HAVE_SPELL:
|
|
from gi.repository import Gspell
|
|
|
|
|
|
################################################################################
|
|
class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
|
"""
|
|
A base class containing a banner, ConversationTextview, MessageTextView
|
|
"""
|
|
|
|
keymap = Gdk.Keymap.get_default()
|
|
try:
|
|
keycode_c = keymap.get_entries_for_keyval(Gdk.KEY_c)[1][0].keycode
|
|
except TypeError:
|
|
keycode_c = 54
|
|
try:
|
|
keycode_ins = keymap.get_entries_for_keyval(Gdk.KEY_Insert)[1][0].keycode
|
|
except TypeError:
|
|
keycode_ins = 118
|
|
except IndexError:
|
|
# There is no KEY_Insert (MacOS)
|
|
keycode_ins = None
|
|
|
|
def make_href(self, match):
|
|
url_color = app.config.get('urlmsgcolor')
|
|
url = match.group()
|
|
if not '://' in url:
|
|
url = 'http://' + url
|
|
return '<a href="%s"><span color="%s">%s</span></a>' % (url,
|
|
url_color, match.group())
|
|
|
|
def get_font_attrs(self):
|
|
"""
|
|
Get pango font attributes for banner from theme settings
|
|
"""
|
|
theme = app.config.get('roster_theme')
|
|
bannerfont = app.config.get_per('themes', theme, 'bannerfont')
|
|
bannerfontattrs = app.config.get_per('themes', theme, 'bannerfontattrs')
|
|
|
|
if bannerfont:
|
|
font = Pango.FontDescription(bannerfont)
|
|
else:
|
|
font = Pango.FontDescription('Normal')
|
|
if bannerfontattrs:
|
|
# B attribute is set by default
|
|
if 'B' in bannerfontattrs:
|
|
font.set_weight(Pango.Weight.HEAVY)
|
|
if 'I' in bannerfontattrs:
|
|
font.set_style(Pango.Style.ITALIC)
|
|
|
|
font_attrs = 'font_desc="%s"' % font.to_string()
|
|
|
|
# in case there is no font specified we use x-large font size
|
|
if font.get_size() == 0:
|
|
font_attrs = '%s size="x-large"' % font_attrs
|
|
font.set_weight(Pango.Weight.NORMAL)
|
|
font_attrs_small = 'font_desc="%s" size="small"' % font.to_string()
|
|
return (font_attrs, font_attrs_small)
|
|
|
|
def get_nb_unread(self):
|
|
jid = self.contact.jid
|
|
if self.resource:
|
|
jid += '/' + self.resource
|
|
type_ = self.type_id
|
|
return len(app.events.get_events(self.account, jid, ['printed_' + type_,
|
|
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()
|
|
app.plugin_manager.gui_extension_point('chat_control_base_draw_banner',
|
|
self)
|
|
|
|
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
|
|
"""
|
|
pass
|
|
|
|
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
|
|
"""
|
|
pass
|
|
|
|
def _update_toolbar(self):
|
|
"""
|
|
Derived types MAY implement this
|
|
"""
|
|
pass
|
|
|
|
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)
|
|
|
|
def _nec_ping_sent(self, obj):
|
|
if self.contact != obj.contact:
|
|
return
|
|
self.print_conversation(_('Ping?'), 'status')
|
|
|
|
def _nec_ping_error(self, obj):
|
|
if self.contact != obj.contact:
|
|
return
|
|
self.print_conversation(_('Error.'), 'status')
|
|
|
|
def status_url_clicked(self, widget, url):
|
|
helpers.launch_browser_mailer('url', url)
|
|
|
|
def setup_seclabel(self, combo):
|
|
self.seclabel_combo = combo
|
|
self.seclabel_combo.hide()
|
|
self.seclabel_combo.set_no_show_all(True)
|
|
lb = Gtk.ListStore(str)
|
|
self.seclabel_combo.set_model(lb)
|
|
cell = Gtk.CellRendererText()
|
|
cell.set_property('xpad', 5) # padding for status text
|
|
self.seclabel_combo.pack_start(cell, True)
|
|
# text to show is in in first column of liststore
|
|
self.seclabel_combo.add_attribute(cell, 'text', 0)
|
|
if app.connections[self.account].seclabel_supported:
|
|
app.connections[self.account].seclabel_catalogue(self.contact.jid, self.on_seclabels_ready)
|
|
|
|
def on_seclabels_ready(self):
|
|
lb = self.seclabel_combo.get_model()
|
|
lb.clear()
|
|
i = 0
|
|
sel = 0
|
|
catalogue = app.connections[self.account].seclabel_catalogues[
|
|
self.contact.jid]
|
|
for label in catalogue[2]:
|
|
lb.append([label])
|
|
if label == catalogue[3]:
|
|
sel = i
|
|
i += 1
|
|
self.seclabel_combo.set_active(sel)
|
|
self.seclabel_combo.set_no_show_all(False)
|
|
self.seclabel_combo.show_all()
|
|
|
|
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.
|
|
c = app.contacts.get_contact_with_highest_priority(acct,
|
|
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)
|
|
|
|
if self.TYPE_ID != message_control.TYPE_GC:
|
|
# Create banner and connect signals
|
|
widget = self.xml.get_object('banner_eventbox')
|
|
id_ = widget.connect('button-press-event',
|
|
self._on_banner_eventbox_button_press_event)
|
|
self.handlers[id_] = widget
|
|
|
|
self.urlfinder = re.compile(
|
|
r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]")
|
|
|
|
self.banner_status_label = self.xml.get_object('banner_label')
|
|
if self.banner_status_label is not None:
|
|
id_ = self.banner_status_label.connect('populate_popup',
|
|
self.on_banner_label_populate_popup)
|
|
self.handlers[id_] = self.banner_status_label
|
|
|
|
# Init DND
|
|
self.TARGET_TYPE_URI_LIST = 80
|
|
self.dnd_list = [Gtk.TargetEntry.new('text/uri-list', 0,
|
|
self.TARGET_TYPE_URI_LIST), Gtk.TargetEntry.new('MY_TREE_MODEL_ROW',
|
|
Gtk.TargetFlags.SAME_APP, 0)]
|
|
id_ = self.widget.connect('drag_data_received',
|
|
self._on_drag_data_received)
|
|
self.handlers[id_] = self.widget
|
|
self.widget.drag_dest_set(Gtk.DestDefaults.MOTION |
|
|
Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP,
|
|
self.dnd_list, Gdk.DragAction.COPY)
|
|
|
|
# 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
|
|
id_ = self.conv_textview.tv.connect('key_press_event',
|
|
self._conv_textview_key_press_event)
|
|
self.handlers[id_] = self.conv_textview.tv
|
|
# FIXME: DND on non editable TextView, find a better way
|
|
self.drag_entered = False
|
|
id_ = self.conv_textview.tv.connect('drag_data_received',
|
|
self._on_drag_data_received)
|
|
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
|
|
self.conv_textview.tv.drag_dest_set(Gtk.DestDefaults.MOTION |
|
|
Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP,
|
|
self.dnd_list, Gdk.DragAction.COPY)
|
|
|
|
self.conv_scrolledwindow = self.xml.get_object(
|
|
'conversation_scrolledwindow')
|
|
self.conv_scrolledwindow.add(self.conv_textview.tv)
|
|
widget = self.conv_scrolledwindow.get_vadjustment()
|
|
id_ = widget.connect('value-changed',
|
|
self.on_conversation_vadjustment_value_changed)
|
|
self.handlers[id_] = widget
|
|
id_ = widget.connect('changed',
|
|
self.on_conversation_vadjustment_changed)
|
|
self.handlers[id_] = widget
|
|
self.was_at_the_end = True
|
|
self.correcting = False
|
|
self.last_sent_msg = None
|
|
|
|
# add MessageTextView to UI and connect signals
|
|
self.msg_textview = MessageTextView()
|
|
self.msg_scrolledwindow = ScrolledWindow()
|
|
self.msg_scrolledwindow.add(self.msg_textview)
|
|
|
|
hbox = self.xml.get_object('hbox')
|
|
hbox.pack_start(self.msg_scrolledwindow, True, True, 0)
|
|
|
|
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
|
|
# Setup DND
|
|
id_ = self.msg_textview.connect('drag_data_received',
|
|
self._on_drag_data_received)
|
|
self.handlers[id_] = self.msg_textview
|
|
self.msg_textview.drag_dest_set(Gtk.DestDefaults.MOTION |
|
|
Gtk.DestDefaults.HIGHLIGHT, self.dnd_list, Gdk.DragAction.COPY)
|
|
|
|
# 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
|
|
|
|
self.set_emoticon_popover()
|
|
|
|
# Attach speller
|
|
self.spell_checker = None
|
|
self.set_speller()
|
|
self.conv_textview.tv.show()
|
|
|
|
# For XEP-0172
|
|
self.user_nick = None
|
|
|
|
self.command_hits = []
|
|
self.last_key_tabs = False
|
|
|
|
# chatstate timers and state
|
|
self.reset_kbd_mouse_timeout_vars()
|
|
self.possible_paused_timeout_id = None
|
|
self.possible_inactive_timeout_id = None
|
|
message_tv_buffer = self.msg_textview.get_buffer()
|
|
id_ = message_tv_buffer.connect('changed',
|
|
self._on_message_tv_buffer_changed)
|
|
self.handlers[id_] = message_tv_buffer
|
|
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._schedule_activity_timers()
|
|
|
|
self.encryption = self.get_encryption_state()
|
|
|
|
# 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)
|
|
|
|
app.ged.register_event_handler('our-show', ged.GUI1,
|
|
self._nec_our_status)
|
|
app.ged.register_event_handler('ping-sent', ged.GUI1,
|
|
self._nec_ping_sent)
|
|
app.ged.register_event_handler('ping-reply', ged.GUI1,
|
|
self._nec_ping_reply)
|
|
app.ged.register_event_handler('ping-error', ged.GUI1,
|
|
self._nec_ping_error)
|
|
|
|
# This is bascially a very nasty hack to surpass the inability
|
|
# to properly use the super, because of the old code.
|
|
CommandTools.__init__(self)
|
|
|
|
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)
|
|
|
|
action = Gio.SimpleAction.new(
|
|
'browse-history-%s' % self.control_id, GLib.VariantType.new('s'))
|
|
action.connect('activate', self._on_history)
|
|
self.parent_win.window.add_action(action)
|
|
|
|
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)
|
|
|
|
# Actions
|
|
|
|
def _on_history(self, action, param):
|
|
"""
|
|
When history menuitem is pressed: call history window
|
|
"""
|
|
jid = param.get_string()
|
|
if jid == 'none':
|
|
jid = self.contact.jid
|
|
|
|
if 'logs' in app.interface.instances:
|
|
app.interface.instances['logs'].window.present()
|
|
app.interface.instances['logs'].open_history(jid, self.account)
|
|
else:
|
|
app.interface.instances['logs'] = \
|
|
history_window.HistoryWindow(jid, self.account)
|
|
|
|
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
|
|
else:
|
|
if not self.widget_name == 'groupchat_control':
|
|
self.terminate_esessions()
|
|
action.set_state(param)
|
|
self.set_encryption_state(encryption)
|
|
self.set_encryption_menu_icon()
|
|
self.set_lock_image()
|
|
|
|
def set_encryption_state(self, encryption):
|
|
config_key = '%s-%s' % (self.account, self.contact.jid)
|
|
self.encryption = encryption
|
|
app.config.set_per('encryption', config_key,
|
|
'encryption', self.encryption or '')
|
|
|
|
def get_encryption_state(self):
|
|
config_key = '%s-%s' % (self.account, self.contact.jid)
|
|
state = app.config.get_per('encryption', config_key, '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.encryption_menu.get_image()
|
|
if image is None:
|
|
image = Gtk.Image()
|
|
self.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.HAVE_SPELL or not app.config.get('use_speller'):
|
|
return
|
|
|
|
gspell_lang = self.get_speller_language()
|
|
if gspell_lang is None:
|
|
return
|
|
|
|
self.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(self.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)
|
|
|
|
self.spell_checker.connect('notify::language', self.on_language_changed)
|
|
|
|
def get_speller_language(self):
|
|
per_type = 'contacts'
|
|
if self.type_id == 'gc':
|
|
per_type = 'rooms'
|
|
lang = app.config.get_per(
|
|
per_type, self.contact.jid, 'speller_language')
|
|
if not lang:
|
|
# use the default one
|
|
lang = app.config.get('speller_language')
|
|
if not lang:
|
|
lang = app.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()
|
|
per_type = 'contacts'
|
|
if self.type_id == message_control.TYPE_GC:
|
|
per_type = 'rooms'
|
|
if not app.config.get_per(per_type, self.contact.jid):
|
|
app.config.add_per(per_type, self.contact.jid)
|
|
app.config.set_per(per_type, self.contact.jid,
|
|
'speller_language', gspell_lang.get_code())
|
|
|
|
def on_banner_label_populate_popup(self, label, menu):
|
|
"""
|
|
Override the default context menu and add our own menutiems
|
|
"""
|
|
item = Gtk.SeparatorMenuItem.new()
|
|
menu.prepend(item)
|
|
|
|
menu2 = self.prepare_context_menu()
|
|
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()
|
|
# Disconnect timer callbacks
|
|
if self.possible_paused_timeout_id:
|
|
GLib.source_remove(self.possible_paused_timeout_id)
|
|
if self.possible_inactive_timeout_id:
|
|
GLib.source_remove(self.possible_inactive_timeout_id)
|
|
# PluginSystem: removing GUI extension points connected with ChatControlBase
|
|
# instance object
|
|
app.plugin_manager.remove_gui_extension_point('chat_control_base',
|
|
self)
|
|
app.plugin_manager.remove_gui_extension_point(
|
|
'chat_control_base_draw_banner', self)
|
|
app.ged.remove_event_handler('our-show', ged.GUI1,
|
|
self._nec_our_status)
|
|
|
|
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
|
|
|
|
menu.show_all()
|
|
|
|
def on_quote(self, widget, text):
|
|
self.msg_textview.remove_placeholder()
|
|
text = '>' + text.replace('\n', '\n>') + '\n'
|
|
message_buffer = self.msg_textview.get_buffer()
|
|
message_buffer.insert_at_cursor(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 _conv_textview_key_press_event(self, widget, event):
|
|
# translate any layout to latin_layout
|
|
valid, entries = self.keymap.get_entries_for_keyval(event.keyval)
|
|
keycode = entries[0].keycode
|
|
if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and keycode in (
|
|
self.keycode_c, self.keycode_ins)) or (
|
|
event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
|
|
event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up)):
|
|
return False
|
|
self.parent_win.notebook.event(event)
|
|
return True
|
|
|
|
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):
|
|
# If the space key has been pressed and now it hasnt,
|
|
# we save the buffer into the undo list. But be carefull we're not
|
|
# pressiong Control again (as in ctrl+z)
|
|
_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
|
|
elif event.keyval == Gdk.KEY_Page_Down or \
|
|
event.keyval == Gdk.KEY_Page_Up:
|
|
self.conv_textview.tv.event(event)
|
|
return True
|
|
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
|
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')
|
|
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
|
|
message_textview = widget
|
|
message_buffer = message_textview.get_buffer()
|
|
emoticons.replace_with_codepoint(message_buffer)
|
|
start_iter, end_iter = message_buffer.get_bounds()
|
|
message = message_buffer.get_text(start_iter, end_iter, False)
|
|
xhtml = self.msg_textview.get_xhtml()
|
|
|
|
if event_state & Gdk.ModifierType.SHIFT_MASK:
|
|
send_message = False
|
|
else:
|
|
is_ctrl_enter = bool(event_state & Gdk.ModifierType.CONTROL_MASK)
|
|
send_message = is_ctrl_enter == app.config.get('send_on_ctrl_enter')
|
|
|
|
if send_message and app.connections[self.account].connected < 2:
|
|
# we are not connected
|
|
app.interface.raise_dialog('not-connected-while-sending')
|
|
elif send_message:
|
|
self.send_message(message, xhtml=xhtml)
|
|
else:
|
|
message_buffer.insert_at_cursor('\n')
|
|
mark = message_buffer.get_insert()
|
|
iter_ = message_buffer.get_iter_at_mark(mark)
|
|
if message_buffer.get_end_iter().equal(iter_):
|
|
GLib.idle_add(gtkgui_helpers.scroll_to_end, self.msg_scrolledwindow)
|
|
|
|
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
|
|
"""
|
|
pass
|
|
|
|
def _on_drag_leave(self, widget, context, time):
|
|
# FIXME: DND on non editable TextView, find a better way
|
|
self.drag_entered = False
|
|
self.conv_textview.tv.set_editable(False)
|
|
|
|
def _on_drag_motion(self, widget, context, x, y, time):
|
|
# 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)
|
|
|
|
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
|
|
con.check_file_before_transfer(
|
|
path, self.encryption, contact,
|
|
self.session, groupchat=True)
|
|
else:
|
|
if httpupload.get_enabled() and jingle.get_enabled():
|
|
if ft_pref == 'httpupload':
|
|
con.check_file_before_transfer(
|
|
path, self.encryption, contact, self.session)
|
|
else:
|
|
ft = app.interface.instances['file_transfers']
|
|
ft.send_file(self.account, contact, path)
|
|
elif httpupload.get_enabled():
|
|
con.check_file_before_transfer(
|
|
path, self.encryption, contact, self.session)
|
|
elif jingle.get_enabled():
|
|
ft = app.interface.instances['file_transfers']
|
|
ft.send_file(self.account, contact, path)
|
|
|
|
def get_seclabel(self):
|
|
label = None
|
|
if self.seclabel_combo is not None:
|
|
idx = self.seclabel_combo.get_active()
|
|
if idx != -1:
|
|
cat = app.connections[self.account].seclabel_catalogues[self.contact.jid]
|
|
lname = cat[2][idx]
|
|
label = cat[1][lname]
|
|
return label
|
|
|
|
def send_message(self, message, keyID='', type_='chat', chatstate=None,
|
|
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
|
|
|
|
# refresh timers
|
|
self.reset_kbd_mouse_timeout_vars()
|
|
|
|
notifications = app.config.get('outgoing_chat_state_notifications')
|
|
if (self.contact.jid == app.get_jid_from_account(self.account) or
|
|
notifications == 'disabled'):
|
|
chatstate = None
|
|
|
|
label = self.get_seclabel()
|
|
|
|
if self.correcting and self.last_sent_msg:
|
|
correct_id = self.last_sent_msg
|
|
else:
|
|
correct_id = None
|
|
|
|
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
|
|
account=self.account, jid=self.contact.jid, message=message,
|
|
keyID=keyID, type_=type_, chatstate=chatstate,
|
|
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
|
|
label=label, control=self, attention=attention, correct_id=correct_id,
|
|
automatic_message=False, encryption=self.encryption))
|
|
|
|
# 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 check_for_possible_paused_chatstate(self, arg):
|
|
"""
|
|
Did we move mouse of that window or write something in message textview
|
|
in the last 5 seconds? If yes - we go active for mouse, composing for
|
|
kbd. If not - we go paused if we were previously composing
|
|
"""
|
|
contact = self.contact
|
|
jid = contact.jid
|
|
current_state = contact.our_chatstate
|
|
if current_state is False: # jid doesn't support chatstates
|
|
self.possible_paused_timeout_id = None
|
|
return False # stop looping
|
|
|
|
if current_state == 'composing':
|
|
if not self.kbd_activity_in_last_5_secs:
|
|
if self.msg_textview.has_text():
|
|
self.send_chatstate('paused', self.contact)
|
|
else:
|
|
self.send_chatstate('active', self.contact)
|
|
elif current_state == 'inactive':
|
|
if (self.mouse_over_in_last_5_secs and
|
|
jid == self.parent_win.get_active_jid()):
|
|
self.send_chatstate('active', self.contact)
|
|
|
|
# assume no activity and let the motion-notify or 'insert-text' make them
|
|
# True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
|
|
self.reset_kbd_mouse_timeout_vars()
|
|
return True # loop forever
|
|
|
|
def check_for_possible_inactive_chatstate(self, arg):
|
|
"""
|
|
Did we move mouse over that window or wrote something in message textview
|
|
in the last 30 seconds? if yes - we go active. If no - we go inactive
|
|
"""
|
|
contact = self.contact
|
|
|
|
current_state = contact.our_chatstate
|
|
if current_state is False: # jid doesn't support chatstates
|
|
self.possible_inactive_timeout_id = None
|
|
return False # stop looping
|
|
|
|
if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
|
|
return True # loop forever
|
|
|
|
if not self.mouse_over_in_last_30_secs or \
|
|
self.kbd_activity_in_last_30_secs:
|
|
self.send_chatstate('inactive', contact)
|
|
|
|
# assume no activity and let the motion-notify or 'insert-text' make them
|
|
# True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
|
|
self.reset_kbd_mouse_timeout_vars()
|
|
return True # loop forever
|
|
|
|
def _schedule_activity_timers(self):
|
|
if self.possible_paused_timeout_id:
|
|
GLib.source_remove(self.possible_paused_timeout_id)
|
|
if self.possible_inactive_timeout_id:
|
|
GLib.source_remove(self.possible_inactive_timeout_id)
|
|
self.possible_paused_timeout_id = GLib.timeout_add_seconds(5,
|
|
self.check_for_possible_paused_chatstate, None)
|
|
self.possible_inactive_timeout_id = GLib.timeout_add_seconds(30,
|
|
self.check_for_possible_inactive_chatstate, None)
|
|
|
|
def reset_kbd_mouse_timeout_vars(self):
|
|
self.kbd_activity_in_last_5_secs = False
|
|
self.mouse_over_in_last_5_secs = False
|
|
self.mouse_over_in_last_30_secs = False
|
|
self.kbd_activity_in_last_30_secs = False
|
|
|
|
def _on_window_motion_notify(self, widget, event):
|
|
"""
|
|
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, change vars assisting chatstate
|
|
self.mouse_over_in_last_5_secs = True
|
|
self.mouse_over_in_last_30_secs = True
|
|
|
|
def _on_message_tv_buffer_changed(self, textbuffer):
|
|
self.kbd_activity_in_last_5_secs = True
|
|
self.kbd_activity_in_last_30_secs = True
|
|
if not self.msg_textview.has_text():
|
|
return
|
|
self.send_chatstate('composing', self.contact)
|
|
|
|
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 = False if pos == size else True # are we scrolling?
|
|
# we don't want size of the buffer to grow indefinately
|
|
max_size = app.config.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 print_conversation_line(self, text, kind, name, tim,
|
|
other_tags_for_name=None, other_tags_for_time=None, other_tags_for_text=None,
|
|
count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False,
|
|
xep0184_id=None, graphics=True, displaymarking=None, msg_log_id=None,
|
|
msg_stanza_id=None, correct_id=None, additional_data=None,
|
|
encrypted=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.was_at_the_end 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 = {}
|
|
|
|
textview.print_conversation_line(text, jid, kind, name, tim,
|
|
other_tags_for_name, other_tags_for_time, other_tags_for_text,
|
|
subject, old_kind, xhtml, simple=simple, graphics=graphics,
|
|
displaymarking=displaymarking, msg_stanza_id=msg_stanza_id,
|
|
correct_id=correct_id, additional_data=additional_data,
|
|
encrypted=encrypted)
|
|
|
|
if xep0184_id is not None:
|
|
textview.add_xep0184_mark(xep0184_id)
|
|
|
|
if not count_as_new:
|
|
return
|
|
if kind == 'incoming':
|
|
if not self.type_id == message_control.TYPE_GC or \
|
|
app.config.get('notify_on_all_muc_messages') or \
|
|
app.config.get_per('rooms', jid, 'notify_on_all_messages') 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')
|
|
|
|
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 \
|
|
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_id == message_control.TYPE_CHAT:
|
|
event_type = events.PrintedChatEvent
|
|
else:
|
|
event_type = events.PrintedPmEvent
|
|
event = 'message_received'
|
|
show_in_roster = notify.get_show_in_roster(event,
|
|
self.account, self.contact, self.session)
|
|
show_in_systray = notify.get_show_in_systray(event,
|
|
self.account, self.contact, event_type.type_)
|
|
|
|
event = event_type(text, subject, self, msg_log_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.config.get('emoticons_theme'):
|
|
self.emoticons_button.set_no_show_all(False)
|
|
self.emoticons_button.show()
|
|
else:
|
|
self.emoticons_button.set_no_show_all(True)
|
|
self.emoticons_button.hide()
|
|
|
|
def set_emoticon_popover(self):
|
|
if not app.config.get('emoticons_theme'):
|
|
return
|
|
|
|
if not self.parent_win:
|
|
return
|
|
|
|
popover = emoticons.get_popover()
|
|
popover.set_callbacks(self.msg_textview)
|
|
emoticons_button = self.xml.get_object('emoticons_button')
|
|
emoticons_button.set_popover(popover)
|
|
|
|
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 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)
|
|
|
|
def _on_history_menuitem_activate(self, widget=None, jid=None):
|
|
"""
|
|
When history menuitem is pressed: call history window
|
|
"""
|
|
if not jid:
|
|
jid = self.contact.jid
|
|
|
|
if 'logs' in app.interface.instances:
|
|
app.interface.instances['logs'].window.present()
|
|
app.interface.instances['logs'].open_history(jid, self.account)
|
|
else:
|
|
app.interface.instances['logs'] = \
|
|
history_window.HistoryWindow(jid, self.account)
|
|
|
|
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):
|
|
"""
|
|
gc_contact can be set when we are in a groupchat control
|
|
"""
|
|
def _on_ok(c):
|
|
app.interface.instances['file_transfers'].show_file_send_request(
|
|
self.account, c)
|
|
if self.type_id == message_control.TYPE_PM:
|
|
gc_contact = self.gc_contact
|
|
if gc_contact:
|
|
# 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 not in ['admin',
|
|
'owner'] and self_contact.affiliation in ['admin', 'owner']:
|
|
contact = app.contacts.get_contact(self.account, gc_contact.jid)
|
|
if not contact or contact.sub not in ('both', 'to'):
|
|
prim_text = _('Really send file?')
|
|
sec_text = _('If you send a file to %s, he/she will know your '
|
|
'real JID.') % gc_contact.name
|
|
dialog = dialogs.NonModalConfirmationDialog(prim_text,
|
|
sec_text, on_response_ok=(_on_ok, gc_contact))
|
|
dialog.popup()
|
|
return
|
|
_on_ok(gc_contact)
|
|
return
|
|
_on_ok(self.contact)
|
|
|
|
def on_minimize_menuitem_toggled(self, widget):
|
|
"""
|
|
When a grouchat is minimized, unparent the tab, put it in roster etc
|
|
"""
|
|
old_value = True
|
|
non_minimized_gc = app.config.get_per('accounts', self.account,
|
|
'non_minimized_gc').split()
|
|
if self.contact.jid in non_minimized_gc:
|
|
old_value = False
|
|
minimize = widget.get_active()
|
|
if not minimize and not self.contact.jid in non_minimized_gc:
|
|
non_minimized_gc.append(self.contact.jid)
|
|
if minimize and self.contact.jid in non_minimized_gc:
|
|
non_minimized_gc.remove(self.contact.jid)
|
|
if old_value != minimize:
|
|
app.config.set_per('accounts', self.account, 'non_minimized_gc',
|
|
' '.join(non_minimized_gc))
|
|
|
|
def on_notify_menuitem_toggled(self, widget):
|
|
app.config.set_per('rooms', self.contact.jid, 'notify_on_all_messages',
|
|
widget.get_active())
|
|
|
|
def set_control_active(self, state):
|
|
if state:
|
|
self.set_emoticon_popover()
|
|
jid = self.contact.jid
|
|
if self.was_at_the_end:
|
|
# we are at the end
|
|
type_ = ['printed_' + self.type_id]
|
|
if self.type_id == message_control.TYPE_GC:
|
|
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)
|
|
# send chatstate inactive to the one we're leaving
|
|
# and active to the one we visit
|
|
if self.msg_textview.has_text():
|
|
self.send_chatstate('paused', self.contact)
|
|
else:
|
|
self.send_chatstate('active', self.contact)
|
|
self.reset_kbd_mouse_timeout_vars()
|
|
self._schedule_activity_timers()
|
|
else:
|
|
self.send_chatstate('inactive', self.contact)
|
|
|
|
def scroll_to_end_iter(self):
|
|
self.conv_textview.scroll_to_end_iter()
|
|
return False
|
|
|
|
def on_conversation_vadjustment_changed(self, adjustment):
|
|
# used to stay at the end of the textview when we shrink conversation
|
|
# textview.
|
|
if self.was_at_the_end:
|
|
self.scroll_to_end_iter()
|
|
self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value()\
|
|
- adjustment.get_page_size()) < 18
|
|
|
|
def on_conversation_vadjustment_value_changed(self, adjustment):
|
|
self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value() \
|
|
- adjustment.get_page_size()) < 18
|
|
if self.resource:
|
|
jid = self.contact.get_full_jid()
|
|
else:
|
|
jid = self.contact.jid
|
|
types_list = []
|
|
type_ = self.type_id
|
|
if type_ == message_control.TYPE_GC:
|
|
type_ = 'gc_msg'
|
|
types_list = ['printed_' + type_, type_, 'printed_marked_gc_msg']
|
|
else: # Not a GC
|
|
types_list = ['printed_' + type_, type_]
|
|
|
|
if not len(app.events.get_events(self.account, jid, types_list)):
|
|
return
|
|
if not self.parent_win:
|
|
return
|
|
if self.conv_textview.at_the_end() and \
|
|
self.parent_win.get_active_control() == self and \
|
|
self.parent_win.window.is_active():
|
|
# we are at the end
|
|
if self.type_id == message_control.TYPE_GC:
|
|
if not app.events.remove_events(self.account, jid,
|
|
types=types_list):
|
|
self.redraw_after_event_removed(jid)
|
|
elif self.session and self.session.remove_events(types_list):
|
|
# There were events to remove
|
|
self.redraw_after_event_removed(jid)
|
|
|
|
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
|
|
"""
|
|
self.parent_win.redraw_tab(self)
|
|
self.parent_win.show_title()
|
|
# TODO : get the contact and check notify.get_show_in_roster()
|
|
if self.type_id == message_control.TYPE_PM:
|
|
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.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, 'msgcorrectingcolor')
|
|
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, 'msgcorrectingcolor')
|
|
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 widget_set_visible(self, widget, state):
|
|
"""
|
|
Show or hide a widget
|
|
"""
|
|
# make the last message visible, when changing to "full view"
|
|
if not state:
|
|
GLib.idle_add(self.conv_textview.scroll_to_end_iter)
|
|
|
|
widget.set_no_show_all(state)
|
|
if state:
|
|
widget.hide()
|
|
else:
|
|
widget.show_all()
|
|
|
|
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.no_autonegotiation = False
|
|
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.set_shadow_type(Gtk.ShadowType.IN)
|
|
|
|
def do_get_preferred_height(self):
|
|
min_height, natural_height = Gtk.ScrolledWindow.do_get_preferred_height(self)
|
|
# Gtk Bug: If policy is set to Automatic, the ScrolledWindow
|
|
# has a min size of around 46-82 depending on the System. Because
|
|
# we want it smaller, we set policy NEVER if the height is < 90
|
|
# so the ScrolledWindow will shrink to around 26 (1 line heigh).
|
|
# Once it gets over 90 its no problem to restore the policy.
|
|
if natural_height < 90:
|
|
GLib.idle_add(self.set_policy,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.NEVER)
|
|
else:
|
|
GLib.idle_add(self.set_policy,
|
|
Gtk.PolicyType.AUTOMATIC,
|
|
Gtk.PolicyType.AUTOMATIC)
|
|
|
|
return min_height, natural_height
|