385 lines
14 KiB
Python
385 lines
14 KiB
Python
|
# 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 time
|
||
|
|
||
|
import nbxmpp
|
||
|
from gi.repository import Gdk
|
||
|
from gi.repository import Gtk
|
||
|
from gi.repository import GLib
|
||
|
|
||
|
from gajim.common import app
|
||
|
from gajim.common import ged
|
||
|
from gajim.common.i18n import _
|
||
|
from gajim.common.const import StyleAttr
|
||
|
|
||
|
from . import util
|
||
|
from .util import get_builder
|
||
|
from .util import MaxWidthComboBoxText
|
||
|
from .util import EventHelper
|
||
|
from .dialogs import ErrorDialog
|
||
|
from .settings import SettingsDialog
|
||
|
from .const import Setting
|
||
|
from .const import SettingKind
|
||
|
from .const import SettingType
|
||
|
|
||
|
|
||
|
class XMLConsoleWindow(Gtk.ApplicationWindow, EventHelper):
|
||
|
def __init__(self):
|
||
|
Gtk.ApplicationWindow.__init__(self)
|
||
|
EventHelper.__init__(self)
|
||
|
self.set_application(app.app)
|
||
|
self.set_position(Gtk.WindowPosition.CENTER)
|
||
|
self.set_default_size(600, 600)
|
||
|
self.set_resizable(True)
|
||
|
self.set_show_menubar(False)
|
||
|
self.set_name('XMLConsoleWindow')
|
||
|
|
||
|
self.selected_account = None
|
||
|
self._selected_send_account = None
|
||
|
self.presence = True
|
||
|
self.message = True
|
||
|
self.iq = True
|
||
|
self.stream = True
|
||
|
self.incoming = True
|
||
|
self.outgoing = True
|
||
|
self.filter_dialog = None
|
||
|
self.last_stanza = None
|
||
|
self.last_search = ''
|
||
|
|
||
|
self._ui = get_builder('xml_console.ui')
|
||
|
self.set_titlebar(self._ui.headerbar)
|
||
|
self._set_titlebar()
|
||
|
self.add(self._ui.box)
|
||
|
|
||
|
self._ui.paned.set_position(self._ui.paned.get_property('max-position'))
|
||
|
|
||
|
self._combo = MaxWidthComboBoxText()
|
||
|
self._combo.set_max_size(200)
|
||
|
self._combo.set_hexpand(False)
|
||
|
self._combo.set_halign(Gtk.Align.END)
|
||
|
self._combo.set_no_show_all(True)
|
||
|
self._combo.set_visible(False)
|
||
|
self._combo.connect('changed', self._on_value_change)
|
||
|
for account, label in self._get_accounts():
|
||
|
self._combo.append(account, label)
|
||
|
self._ui.actionbar.pack_end(self._combo)
|
||
|
|
||
|
self._create_tags()
|
||
|
self.show_all()
|
||
|
|
||
|
self.connect('key_press_event', self._on_key_press_event)
|
||
|
self._ui.connect_signals(self)
|
||
|
|
||
|
self.register_events([
|
||
|
('stanza-received', ged.GUI1, self._nec_stanza_received),
|
||
|
('stanza-sent', ged.GUI1, self._nec_stanza_sent),
|
||
|
])
|
||
|
|
||
|
def _on_value_change(self, combo):
|
||
|
self._selected_send_account = combo.get_active_id()
|
||
|
|
||
|
def _set_titlebar(self):
|
||
|
if self.selected_account is None:
|
||
|
title = _('All Accounts')
|
||
|
else:
|
||
|
title = app.get_jid_from_account(self.selected_account)
|
||
|
self._ui.headerbar.set_subtitle(title)
|
||
|
|
||
|
def _create_tags(self):
|
||
|
buffer_ = self._ui.textview.get_buffer()
|
||
|
in_color = app.css_config.get_value(
|
||
|
'.gajim-incoming-nickname', StyleAttr.COLOR)
|
||
|
out_color = app.css_config.get_value(
|
||
|
'.gajim-outgoing-nickname', StyleAttr.COLOR)
|
||
|
|
||
|
tags = ['presence', 'message', 'stream', 'iq']
|
||
|
|
||
|
tag = buffer_.create_tag('incoming')
|
||
|
tag.set_property('foreground', in_color)
|
||
|
tag = buffer_.create_tag('outgoing')
|
||
|
tag.set_property('foreground', out_color)
|
||
|
|
||
|
for tag_name in tags:
|
||
|
buffer_.create_tag(tag_name)
|
||
|
|
||
|
def _on_key_press_event(self, _widget, event):
|
||
|
if event.keyval == Gdk.KEY_Escape:
|
||
|
if self._ui.search_revealer.get_child_revealed():
|
||
|
self._ui.search_revealer.set_reveal_child(False)
|
||
|
return
|
||
|
self.destroy()
|
||
|
if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and
|
||
|
event.keyval == Gdk.KEY_Return or
|
||
|
event.keyval == Gdk.KEY_KP_Enter):
|
||
|
self._on_send()
|
||
|
if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and
|
||
|
event.keyval == Gdk.KEY_Up):
|
||
|
self._on_paste_last()
|
||
|
if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and
|
||
|
event.keyval == Gdk.KEY_f):
|
||
|
self._ui.search_toggle.set_active(
|
||
|
not self._ui.search_revealer.get_child_revealed())
|
||
|
if event.keyval == Gdk.KEY_F3:
|
||
|
self._find(True)
|
||
|
|
||
|
def _on_row_activated(self, _listbox, row):
|
||
|
text = row.get_child().get_text()
|
||
|
input_text = None
|
||
|
if text == 'Presence':
|
||
|
input_text = (
|
||
|
'<presence xmlns="jabber:client">\n'
|
||
|
'<show></show>\n'
|
||
|
'<status></status>\n'
|
||
|
'<priority></priority>\n'
|
||
|
'</presence>')
|
||
|
elif text == 'Message':
|
||
|
input_text = (
|
||
|
'<message to="" type="" xmlns="jabber:client">\n'
|
||
|
'<body></body>\n'
|
||
|
'</message>')
|
||
|
elif text == 'Iq':
|
||
|
input_text = (
|
||
|
'<iq to="" type="" xmlns="jabber:client">\n'
|
||
|
'<query xmlns=""></query>\n'
|
||
|
'</iq>')
|
||
|
|
||
|
if input_text is not None:
|
||
|
buffer_ = self._ui.input_entry.get_buffer()
|
||
|
buffer_.set_text(input_text)
|
||
|
self._ui.input_entry.grab_focus()
|
||
|
|
||
|
def _on_send(self, *args):
|
||
|
if not self._selected_send_account:
|
||
|
return
|
||
|
if not app.account_is_available(self._selected_send_account):
|
||
|
# If offline or connecting
|
||
|
ErrorDialog(
|
||
|
_('Connection not available'),
|
||
|
_('Please make sure you are connected with \'%s\'.') %
|
||
|
self._selected_send_account)
|
||
|
return
|
||
|
buffer_ = self._ui.input_entry.get_buffer()
|
||
|
begin_iter, end_iter = buffer_.get_bounds()
|
||
|
stanza = buffer_.get_text(begin_iter, end_iter, True)
|
||
|
if stanza:
|
||
|
try:
|
||
|
node = nbxmpp.Node(node=stanza)
|
||
|
except Exception as error:
|
||
|
ErrorDialog(_('Invalid Node'), str(error))
|
||
|
return
|
||
|
|
||
|
if node.getName() in ('message', 'presence', 'iq'):
|
||
|
# Parse stanza again if its a message, presence or iq and
|
||
|
# set jabber:client as toplevel namespace
|
||
|
# Use type Protocol so nbxmpp counts the stanza for
|
||
|
# stream management
|
||
|
node = nbxmpp.Protocol(node=stanza,
|
||
|
attrs={'xmlns': 'jabber:client'})
|
||
|
app.connections[self._selected_send_account].connection.send(node)
|
||
|
self.last_stanza = stanza
|
||
|
buffer_.set_text('')
|
||
|
|
||
|
def _on_paste_last(self, *args):
|
||
|
buffer_ = self._ui.input_entry.get_buffer()
|
||
|
if buffer_ is not None and self.last_stanza is not None:
|
||
|
buffer_.set_text(self.last_stanza)
|
||
|
self._ui.input_entry.grab_focus()
|
||
|
|
||
|
def _on_input(self, button, *args):
|
||
|
if button.get_active():
|
||
|
self._ui.paned.get_child2().show()
|
||
|
self._ui.send.show()
|
||
|
self._ui.paste.show()
|
||
|
self._combo.show()
|
||
|
self._ui.menubutton.show()
|
||
|
self._ui.input_entry.grab_focus()
|
||
|
else:
|
||
|
self._ui.paned.get_child2().hide()
|
||
|
self._ui.send.hide()
|
||
|
self._ui.paste.hide()
|
||
|
self._combo.hide()
|
||
|
self._ui.menubutton.hide()
|
||
|
|
||
|
def _on_search_toggled(self, button):
|
||
|
self._ui.search_revealer.set_reveal_child(button.get_active())
|
||
|
self._ui.search_entry.grab_focus()
|
||
|
|
||
|
def _on_search_activate(self, _widget):
|
||
|
self._find(True)
|
||
|
|
||
|
def _on_search_clicked(self, button):
|
||
|
forward = bool(button is self._ui.search_forward)
|
||
|
self._find(forward)
|
||
|
|
||
|
def _find(self, forward):
|
||
|
search_str = self._ui.search_entry.get_text()
|
||
|
textbuffer = self._ui.textview.get_buffer()
|
||
|
cursor_mark = textbuffer.get_insert()
|
||
|
current_pos = textbuffer.get_iter_at_mark(cursor_mark)
|
||
|
|
||
|
if current_pos.get_offset() == textbuffer.get_char_count():
|
||
|
current_pos = textbuffer.get_start_iter()
|
||
|
|
||
|
last_pos_mark = textbuffer.get_mark('last_pos')
|
||
|
if last_pos_mark is not None:
|
||
|
current_pos = textbuffer.get_iter_at_mark(last_pos_mark)
|
||
|
|
||
|
if search_str != self.last_search:
|
||
|
current_pos = textbuffer.get_start_iter()
|
||
|
|
||
|
if forward:
|
||
|
match = current_pos.forward_search(
|
||
|
search_str,
|
||
|
Gtk.TextSearchFlags.VISIBLE_ONLY |
|
||
|
Gtk.TextSearchFlags.CASE_INSENSITIVE,
|
||
|
None)
|
||
|
else:
|
||
|
current_pos.backward_cursor_position()
|
||
|
match = current_pos.backward_search(
|
||
|
search_str,
|
||
|
Gtk.TextSearchFlags.VISIBLE_ONLY |
|
||
|
Gtk.TextSearchFlags.CASE_INSENSITIVE,
|
||
|
None)
|
||
|
|
||
|
if match is not None:
|
||
|
match_start, match_end = match
|
||
|
textbuffer.select_range(match_start, match_end)
|
||
|
mark = textbuffer.create_mark('last_pos', match_end, True)
|
||
|
self._ui.textview.scroll_to_mark(mark, 0, True, 0.5, 0.5)
|
||
|
self.last_search = search_str
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_accounts():
|
||
|
accounts = app.get_accounts_sorted()
|
||
|
combo_accounts = []
|
||
|
for account in accounts:
|
||
|
label = app.get_account_label(account)
|
||
|
combo_accounts.append((account, label))
|
||
|
combo_accounts.append(('AccountWizard', 'Account Wizard'))
|
||
|
return combo_accounts
|
||
|
|
||
|
def _on_filter_options(self, *args):
|
||
|
if self.filter_dialog:
|
||
|
self.filter_dialog.present()
|
||
|
return
|
||
|
|
||
|
combo_accounts = self._get_accounts()
|
||
|
combo_accounts.insert(0, (None, _('All Accounts')))
|
||
|
|
||
|
settings = [
|
||
|
Setting(SettingKind.COMBO, _('Account'),
|
||
|
SettingType.VALUE, self.selected_account,
|
||
|
callback=self._set_account,
|
||
|
props={'combo_items': combo_accounts}),
|
||
|
|
||
|
Setting(SettingKind.SWITCH, 'Presence',
|
||
|
SettingType.VALUE, self.presence,
|
||
|
callback=self._on_setting, data='presence'),
|
||
|
|
||
|
Setting(SettingKind.SWITCH, 'Message',
|
||
|
SettingType.VALUE, self.message,
|
||
|
callback=self._on_setting, data='message'),
|
||
|
|
||
|
Setting(SettingKind.SWITCH, 'IQ', SettingType.VALUE, self.iq,
|
||
|
callback=self._on_setting, data='iq'),
|
||
|
|
||
|
Setting(SettingKind.SWITCH, 'Stream Management',
|
||
|
SettingType.VALUE, self.stream,
|
||
|
callback=self._on_setting, data='stream'),
|
||
|
|
||
|
Setting(SettingKind.SWITCH, 'In', SettingType.VALUE, self.incoming,
|
||
|
callback=self._on_setting, data='incoming'),
|
||
|
|
||
|
Setting(SettingKind.SWITCH, 'Out', SettingType.VALUE, self.outgoing,
|
||
|
callback=self._on_setting, data='outgoing'),
|
||
|
]
|
||
|
|
||
|
self.filter_dialog = SettingsDialog(self, _('Filter'),
|
||
|
Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||
|
settings, self.selected_account)
|
||
|
self.filter_dialog.connect('destroy', self._on_filter_destroyed)
|
||
|
|
||
|
def _on_filter_destroyed(self, _win):
|
||
|
self.filter_dialog = None
|
||
|
|
||
|
def _on_clear(self, *args):
|
||
|
self._ui.textview.get_buffer().set_text('')
|
||
|
|
||
|
def _set_account(self, value, _data):
|
||
|
self.selected_account = value
|
||
|
self._set_titlebar()
|
||
|
|
||
|
def _on_setting(self, value, data):
|
||
|
setattr(self, data, value)
|
||
|
value = not value
|
||
|
table = self._ui.textview.get_buffer().get_tag_table()
|
||
|
tag = table.lookup(data)
|
||
|
if data in ('incoming', 'outgoing'):
|
||
|
if value:
|
||
|
tag.set_priority(table.get_size() - 1)
|
||
|
else:
|
||
|
tag.set_priority(0)
|
||
|
tag.set_property('invisible', value)
|
||
|
|
||
|
def _nec_stanza_received(self, event):
|
||
|
if self.selected_account is not None:
|
||
|
if event.account != self.selected_account:
|
||
|
return
|
||
|
self._print_stanza(event, 'incoming')
|
||
|
|
||
|
def _nec_stanza_sent(self, event):
|
||
|
if self.selected_account is not None:
|
||
|
if event.account != self.selected_account:
|
||
|
return
|
||
|
self._print_stanza(event, 'outgoing')
|
||
|
|
||
|
def _print_stanza(self, event, kind):
|
||
|
if event.account == 'AccountWizard':
|
||
|
account_label = 'Account Wizard'
|
||
|
else:
|
||
|
account_label = app.get_account_label(event.account)
|
||
|
|
||
|
stanza = event.stanza
|
||
|
if not isinstance(stanza, str):
|
||
|
stanza = stanza.__str__(fancy=True)
|
||
|
|
||
|
if not stanza:
|
||
|
return
|
||
|
|
||
|
at_the_end = util.at_the_end(self._ui.scrolled)
|
||
|
|
||
|
buffer_ = self._ui.textview.get_buffer()
|
||
|
end_iter = buffer_.get_end_iter()
|
||
|
|
||
|
type_ = kind
|
||
|
if stanza.startswith('<presence'):
|
||
|
type_ = 'presence'
|
||
|
elif stanza.startswith('<message'):
|
||
|
type_ = 'message'
|
||
|
elif stanza.startswith('<iq'):
|
||
|
type_ = 'iq'
|
||
|
elif stanza.startswith('<r') or stanza.startswith('<a'):
|
||
|
type_ = 'stream'
|
||
|
|
||
|
stanza = '<!-- {kind} {time} ({account}) -->\n{stanza}\n\n'.format(
|
||
|
kind=kind.capitalize(),
|
||
|
time=time.strftime('%c'),
|
||
|
account=account_label,
|
||
|
stanza=stanza)
|
||
|
buffer_.insert_with_tags_by_name(end_iter, stanza, type_, kind)
|
||
|
|
||
|
if at_the_end:
|
||
|
GLib.idle_add(util.scroll_to_end, self._ui.scrolled)
|