650 lines
21 KiB
Python
650 lines
21 KiB
Python
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
|
|
#
|
|
# 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/>.
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository import GLib
|
|
from gi.repository import Gdk
|
|
from gi.repository import GObject
|
|
from gi.repository import Pango
|
|
|
|
from gajim.common import app
|
|
from gajim.common import passwords
|
|
from gajim.common.i18n import _
|
|
|
|
from gajim.common.exceptions import GajimGeneralException
|
|
|
|
from gajim import gtkgui_helpers
|
|
|
|
from gajim.gtk.dialogs import ChangePasswordDialog
|
|
from gajim.gtk.util import get_image_button
|
|
from gajim.gtk.util import MaxWidthComboBoxText
|
|
from gajim.gtk.const import SettingKind
|
|
from gajim.gtk.const import SettingType
|
|
|
|
|
|
class SettingsDialog(Gtk.ApplicationWindow):
|
|
def __init__(self, parent, title, flags, settings, account,
|
|
extend=None):
|
|
Gtk.ApplicationWindow.__init__(self)
|
|
self.set_application(app.app)
|
|
self.set_show_menubar(False)
|
|
self.set_title(title)
|
|
self.set_transient_for(parent)
|
|
self.set_resizable(False)
|
|
self.set_default_size(250, -1)
|
|
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
|
self.account = account
|
|
if flags == Gtk.DialogFlags.MODAL:
|
|
self.set_modal(True)
|
|
elif flags == Gtk.DialogFlags.DESTROY_WITH_PARENT:
|
|
self.set_destroy_with_parent(True)
|
|
|
|
self.listbox = SettingsBox(account, extend)
|
|
self.listbox.set_hexpand(True)
|
|
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
|
|
for setting in settings:
|
|
self.listbox.add_setting(setting)
|
|
self.listbox.update_states()
|
|
|
|
self.add(self.listbox)
|
|
|
|
self.show_all()
|
|
self.listbox.connect('row-activated', self.on_row_activated)
|
|
self.connect('key-press-event', self.on_key_press)
|
|
|
|
def on_key_press(self, widget, event):
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
self.destroy()
|
|
|
|
@staticmethod
|
|
def on_row_activated(listbox, row):
|
|
row.on_row_activated()
|
|
|
|
def get_setting(self, name):
|
|
return self.listbox.get_setting(name)
|
|
|
|
|
|
class SettingsBox(Gtk.ListBox):
|
|
def __init__(self, account, extend=None):
|
|
Gtk.ListBox.__init__(self)
|
|
self.get_style_context().add_class('settings-box')
|
|
self.account = account
|
|
self.named_settings = {}
|
|
|
|
self.map = {
|
|
SettingKind.SWITCH: SwitchSetting,
|
|
SettingKind.SPIN: SpinSetting,
|
|
SettingKind.DIALOG: DialogSetting,
|
|
SettingKind.ENTRY: EntrySetting,
|
|
SettingKind.ACTION: ActionSetting,
|
|
SettingKind.LOGIN: LoginSetting,
|
|
SettingKind.FILECHOOSER: FileChooserSetting,
|
|
SettingKind.CALLBACK: CallbackSetting,
|
|
SettingKind.PROXY: ProxyComboSetting,
|
|
SettingKind.PRIORITY: PrioritySetting,
|
|
SettingKind.HOSTNAME: CutstomHostnameSetting,
|
|
SettingKind.CHANGEPASSWORD: ChangePasswordSetting,
|
|
SettingKind.COMBO: ComboSetting,
|
|
SettingKind.CHATSTATE_COMBO: ChatstateComboSetting,
|
|
}
|
|
|
|
if extend is not None:
|
|
for setting, callback in extend:
|
|
self.map[setting] = callback
|
|
|
|
def add_setting(self, setting):
|
|
if not isinstance(setting, Gtk.ListBoxRow):
|
|
if setting.props is not None:
|
|
listitem = self.map[setting.kind](
|
|
self.account, *setting[1:-1], **setting.props)
|
|
else:
|
|
listitem = self.map[setting.kind](self.account, *setting[1:-1])
|
|
listitem.connect('notify::setting-value', self.on_setting_changed)
|
|
if setting.name is not None:
|
|
self.named_settings[setting.name] = listitem
|
|
self.add(listitem)
|
|
|
|
def get_setting(self, name):
|
|
return self.named_settings[name]
|
|
|
|
def update_states(self):
|
|
values = []
|
|
values.append((None, None))
|
|
for row in self.get_children():
|
|
name = row.name
|
|
if name is None:
|
|
continue
|
|
value = row.get_property('setting-value')
|
|
values.append((name, value))
|
|
|
|
for name, value in values:
|
|
for row in self.get_children():
|
|
row.update_activatable(name, value)
|
|
|
|
def on_setting_changed(self, widget, *args):
|
|
value = widget.get_property('setting-value')
|
|
for row in self.get_children():
|
|
row.update_activatable(widget.name, value)
|
|
|
|
|
|
class GenericSetting(Gtk.ListBoxRow):
|
|
def __init__(self, account, label, type_, value,
|
|
name, callback, data, desc, enabledif):
|
|
Gtk.ListBoxRow.__init__(self)
|
|
self._grid = Gtk.Grid()
|
|
self._grid.set_size_request(-1, 30)
|
|
self._grid.set_column_spacing(12)
|
|
|
|
self.callback = callback
|
|
self.type_ = type_
|
|
self.value = value
|
|
self.data = data
|
|
self.label = label
|
|
self.account = account
|
|
self.name = name
|
|
self.enabledif = enabledif
|
|
self.setting_value = self.get_value()
|
|
|
|
description_box = Gtk.Box(
|
|
orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
|
description_box.set_valign(Gtk.Align.CENTER)
|
|
|
|
settingtext = Gtk.Label(label=label)
|
|
settingtext.set_hexpand(True)
|
|
settingtext.set_halign(Gtk.Align.START)
|
|
settingtext.set_valign(Gtk.Align.CENTER)
|
|
settingtext.set_vexpand(True)
|
|
description_box.add(settingtext)
|
|
|
|
if desc is not None:
|
|
description = Gtk.Label(label=desc)
|
|
description.set_name('SubDescription')
|
|
description.set_hexpand(True)
|
|
description.set_halign(Gtk.Align.START)
|
|
description.set_valign(Gtk.Align.CENTER)
|
|
description.set_xalign(0)
|
|
description.set_line_wrap(True)
|
|
description.set_line_wrap_mode(Pango.WrapMode.WORD)
|
|
description.set_max_width_chars(50)
|
|
description_box.add(description)
|
|
|
|
self._grid.add(description_box)
|
|
|
|
self.setting_box = Gtk.Box(spacing=6)
|
|
self.setting_box.set_size_request(200, -1)
|
|
self.setting_box.set_valign(Gtk.Align.CENTER)
|
|
self.setting_box.set_name('GenericSettingBox')
|
|
self._grid.add(self.setting_box)
|
|
self.add(self._grid)
|
|
|
|
def do_get_property(self, prop):
|
|
if prop.name == 'setting-value':
|
|
return self.setting_value
|
|
raise AttributeError('unknown property %s' % prop.name)
|
|
|
|
def do_set_property(self, prop, value):
|
|
if prop.name == 'setting-value':
|
|
self.setting_value = value
|
|
else:
|
|
raise AttributeError('unknown property %s' % prop.name)
|
|
|
|
def get_value(self):
|
|
return self.__get_value(self.type_, self.value, self.account)
|
|
|
|
@staticmethod
|
|
def __get_value(type_, value, account):
|
|
if value is None:
|
|
return
|
|
if type_ == SettingType.VALUE:
|
|
return value
|
|
|
|
if type_ == SettingType.CONFIG:
|
|
return app.config.get(value)
|
|
|
|
if type_ == SettingType.ACCOUNT_CONFIG:
|
|
if value == 'password':
|
|
return passwords.get_password(account)
|
|
if value == 'no_log_for':
|
|
no_log = app.config.get_per(
|
|
'accounts', account, 'no_log_for').split()
|
|
return account not in no_log
|
|
return app.config.get_per('accounts', account, value)
|
|
|
|
if type_ == SettingType.ACTION:
|
|
if value.startswith('-'):
|
|
return account + value
|
|
return value
|
|
|
|
raise ValueError('Wrong SettingType?')
|
|
|
|
def set_value(self, state):
|
|
if self.type_ == SettingType.CONFIG:
|
|
app.config.set(self.value, state)
|
|
if self.type_ == SettingType.ACCOUNT_CONFIG:
|
|
if self.value == 'password':
|
|
passwords.save_password(self.account, state)
|
|
if self.value == 'no_log_for':
|
|
self.set_no_log_for(self.account, state)
|
|
else:
|
|
app.config.set_per('accounts', self.account, self.value, state)
|
|
|
|
if self.callback is not None:
|
|
self.callback(state, self.data)
|
|
|
|
self.set_property('setting-value', state)
|
|
|
|
@staticmethod
|
|
def set_no_log_for(account, state):
|
|
no_log = app.config.get_per('accounts', account, 'no_log_for').split()
|
|
if state and account in no_log:
|
|
no_log.remove(account)
|
|
elif not state and account not in no_log:
|
|
no_log.append(account)
|
|
app.config.set_per('accounts', account, 'no_log_for', ' '.join(no_log))
|
|
|
|
def on_row_activated(self):
|
|
raise NotImplementedError
|
|
|
|
def update_activatable(self, name, value):
|
|
if self.enabledif is None or self.enabledif[0] != name:
|
|
return
|
|
activatable = (name, value) == self.enabledif
|
|
self.set_activatable(activatable)
|
|
self.set_sensitive(activatable)
|
|
|
|
|
|
class SwitchSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (bool, 'Switch Value', '', False,
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args):
|
|
GenericSetting.__init__(self, *args)
|
|
|
|
self.switch = Gtk.Switch()
|
|
if self.type_ == SettingType.ACTION:
|
|
self.switch.set_action_name('app.%s' % self.setting_value)
|
|
state = app.app.get_action_state(self.setting_value)
|
|
self.switch.set_active(state.get_boolean())
|
|
else:
|
|
self.switch.set_active(self.setting_value)
|
|
self.switch.connect('notify::active', self.on_switch)
|
|
self.switch.set_hexpand(True)
|
|
self.switch.set_halign(Gtk.Align.END)
|
|
self.switch.set_valign(Gtk.Align.CENTER)
|
|
|
|
self.setting_box.add(self.switch)
|
|
|
|
self.show_all()
|
|
|
|
def on_row_activated(self):
|
|
state = self.switch.get_active()
|
|
self.switch.set_active(not state)
|
|
|
|
def on_switch(self, switch, *args):
|
|
value = switch.get_active()
|
|
self.set_value(value)
|
|
|
|
|
|
class EntrySetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Entry Value', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args):
|
|
GenericSetting.__init__(self, *args)
|
|
|
|
self.entry = Gtk.Entry()
|
|
self.entry.set_text(str(self.setting_value))
|
|
self.entry.connect('notify::text', self.on_text_change)
|
|
self.entry.set_valign(Gtk.Align.CENTER)
|
|
self.entry.set_alignment(1)
|
|
|
|
if self.value == 'password':
|
|
self.entry.set_invisible_char('*')
|
|
self.entry.set_visibility(False)
|
|
|
|
self.setting_box.pack_end(self.entry, True, True, 0)
|
|
|
|
self.show_all()
|
|
|
|
def on_text_change(self, *args):
|
|
text = self.entry.get_text()
|
|
self.set_value(text)
|
|
|
|
def on_row_activated(self):
|
|
self.entry.grab_focus()
|
|
|
|
|
|
class DialogSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Dummy', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args, dialog):
|
|
GenericSetting.__init__(self, *args)
|
|
self.dialog = dialog
|
|
|
|
self.setting_value = Gtk.Label()
|
|
self.setting_value.set_text(self.get_setting_value())
|
|
self.setting_value.set_halign(Gtk.Align.END)
|
|
self.setting_box.pack_start(self.setting_value, True, True, 0)
|
|
|
|
self.show_all()
|
|
|
|
def show_dialog(self, parent):
|
|
if self.dialog:
|
|
dialog = self.dialog(self.account, parent)
|
|
dialog.connect('destroy', self.on_destroy)
|
|
|
|
def on_destroy(self, *args):
|
|
self.setting_value.set_text(self.get_setting_value())
|
|
|
|
def get_setting_value(self):
|
|
self.setting_value.hide()
|
|
return ''
|
|
|
|
def on_row_activated(self):
|
|
self.show_dialog(self.get_toplevel())
|
|
|
|
|
|
class SpinSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (int, 'Priority', '', -128, 127, 0,
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args, range_):
|
|
GenericSetting.__init__(self, *args)
|
|
|
|
lower, upper = range_
|
|
adjustment = Gtk.Adjustment(value=0,
|
|
lower=lower,
|
|
upper=upper,
|
|
step_increment=1,
|
|
page_increment=10,
|
|
page_size=0)
|
|
|
|
self.spin = Gtk.SpinButton()
|
|
self.spin.set_adjustment(adjustment)
|
|
self.spin.set_numeric(True)
|
|
self.spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID)
|
|
self.spin.set_value(self.setting_value)
|
|
self.spin.set_halign(Gtk.Align.END)
|
|
self.spin.set_valign(Gtk.Align.CENTER)
|
|
self.spin.connect('notify::value', self.on_value_change)
|
|
|
|
self.setting_box.pack_start(self.spin, True, True, 0)
|
|
|
|
self.show_all()
|
|
|
|
def on_row_activated(self):
|
|
self.spin.grab_focus()
|
|
|
|
def on_value_change(self, spin, *args):
|
|
value = spin.get_value_as_int()
|
|
self.set_value(value)
|
|
|
|
|
|
class FileChooserSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Certificate Path', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args, filefilter):
|
|
GenericSetting.__init__(self, *args)
|
|
|
|
button = Gtk.FileChooserButton(title=self.label,
|
|
action=Gtk.FileChooserAction.OPEN)
|
|
button.set_halign(Gtk.Align.END)
|
|
|
|
# GTK Bug: The FileChooserButton expands without limit
|
|
# get the label and use set_max_wide_chars()
|
|
label = button.get_children()[0].get_children()[0].get_children()[1]
|
|
label.set_max_width_chars(20)
|
|
|
|
if filefilter:
|
|
name, pattern = filefilter
|
|
filter_ = Gtk.FileFilter()
|
|
filter_.set_name(name)
|
|
filter_.add_pattern(pattern)
|
|
button.add_filter(filter_)
|
|
button.set_filter(filter_)
|
|
|
|
filter_ = Gtk.FileFilter()
|
|
filter_.set_name(_('All files'))
|
|
filter_.add_pattern('*')
|
|
button.add_filter(filter_)
|
|
|
|
if self.setting_value:
|
|
button.set_filename(self.setting_value)
|
|
button.connect('selection-changed', self.on_select)
|
|
|
|
clear_button = get_image_button(
|
|
'edit-clear-all-symbolic', _('Clear File'))
|
|
clear_button.connect('clicked', lambda *args: button.unselect_all())
|
|
self.setting_box.pack_start(button, True, True, 0)
|
|
self.setting_box.pack_start(clear_button, False, False, 0)
|
|
|
|
self.show_all()
|
|
|
|
def on_select(self, filechooser):
|
|
self.set_value(filechooser.get_filename() or '')
|
|
|
|
def on_row_activated(self):
|
|
pass
|
|
|
|
|
|
class CallbackSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Dummy', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args, callback):
|
|
GenericSetting.__init__(self, *args)
|
|
self.callback = callback
|
|
self.show_all()
|
|
|
|
def on_row_activated(self):
|
|
self.callback()
|
|
|
|
|
|
class ActionSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Dummy', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args, account):
|
|
GenericSetting.__init__(self, *args)
|
|
action_name = '%s%s' % (account, self.value)
|
|
self.action = gtkgui_helpers.get_action(action_name)
|
|
self.variant = GLib.Variant.new_string(account)
|
|
self.on_enable()
|
|
|
|
self.show_all()
|
|
self.action.connect('notify::enabled', self.on_enable)
|
|
|
|
def on_enable(self, *args):
|
|
self.set_sensitive(self.action.get_enabled())
|
|
|
|
def on_row_activated(self):
|
|
self.action.activate(self.variant)
|
|
|
|
|
|
class LoginSetting(DialogSetting):
|
|
def __init__(self, *args, **kwargs):
|
|
DialogSetting.__init__(self, *args, **kwargs)
|
|
self.setting_value.set_selectable(True)
|
|
|
|
def get_setting_value(self):
|
|
jid = app.get_jid_from_account(self.account)
|
|
return jid
|
|
|
|
def update_activatable(self, name, value):
|
|
super().update_activatable(name, value)
|
|
anonym = app.config.get_per('accounts', self.account, 'anonymous_auth')
|
|
self.set_activatable(not anonym)
|
|
|
|
|
|
class ComboSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Proxy', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args, combo_items):
|
|
GenericSetting.__init__(self, *args)
|
|
|
|
self.combo = MaxWidthComboBoxText()
|
|
self.combo.set_valign(Gtk.Align.CENTER)
|
|
text_renderer = self.combo.get_cells()[0]
|
|
text_renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
|
|
for index, value in enumerate(combo_items):
|
|
if isinstance(value, tuple):
|
|
value, label = value
|
|
self.combo.append(value, _(label))
|
|
else:
|
|
self.combo.append(value, value)
|
|
if value == self.setting_value or index == 0:
|
|
self.combo.set_active(index)
|
|
|
|
self.combo.connect('changed', self.on_value_change)
|
|
|
|
self.setting_box.pack_start(self.combo, True, True, 0)
|
|
self.show_all()
|
|
|
|
def on_value_change(self, combo):
|
|
self.set_value(combo.get_active_id())
|
|
|
|
|
|
class ChatstateComboSetting(ComboSetting):
|
|
def on_value_change(self, combo):
|
|
self.set_value(combo.get_active_id())
|
|
if 'muc' in self.value:
|
|
app.config.del_all_per('rooms', 'send_chatstate')
|
|
else:
|
|
app.config.del_all_per('contacts', 'send_chatstate')
|
|
|
|
|
|
class ProxyComboSetting(GenericSetting):
|
|
|
|
__gproperties__ = {
|
|
"setting-value": (str, 'Proxy', '', '',
|
|
GObject.ParamFlags.READWRITE),}
|
|
|
|
def __init__(self, *args):
|
|
GenericSetting.__init__(self, *args)
|
|
|
|
self.combo = MaxWidthComboBoxText()
|
|
self.combo.set_valign(Gtk.Align.CENTER)
|
|
text_renderer = self.combo.get_cells()[0]
|
|
text_renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
|
|
|
|
self._signal_id = None
|
|
self.update_values()
|
|
|
|
button = get_image_button(
|
|
'preferences-system-symbolic', _('Manage Proxies'))
|
|
button.set_action_name('app.manage-proxies')
|
|
button.set_valign(Gtk.Align.CENTER)
|
|
|
|
self.setting_box.pack_start(self.combo, True, True, 0)
|
|
self.setting_box.pack_start(button, False, True, 0)
|
|
self.show_all()
|
|
|
|
def _block_signal(self, state):
|
|
if state:
|
|
if self._signal_id is None:
|
|
return
|
|
self.combo.disconnect(self._signal_id)
|
|
else:
|
|
self._signal_id = self.combo.connect('changed',
|
|
self.on_value_change)
|
|
self.combo.emit('changed')
|
|
|
|
def update_values(self):
|
|
self._block_signal(True)
|
|
proxies = app.config.get_per('proxies')
|
|
proxies.insert(0, _('No Proxy'))
|
|
self.combo.remove_all()
|
|
for index, value in enumerate(proxies):
|
|
self.combo.insert_text(-1, value)
|
|
if value == self.setting_value or index == 0:
|
|
self.combo.set_active(index)
|
|
self._block_signal(False)
|
|
|
|
def on_value_change(self, combo):
|
|
if combo.get_active() == 0:
|
|
self.set_value('')
|
|
else:
|
|
self.set_value(combo.get_active_text())
|
|
|
|
def on_row_activated(self):
|
|
pass
|
|
|
|
|
|
class PrioritySetting(DialogSetting):
|
|
def __init__(self, *args, **kwargs):
|
|
DialogSetting.__init__(self, *args, **kwargs)
|
|
|
|
def get_setting_value(self):
|
|
adjust = app.config.get_per(
|
|
'accounts', self.account, 'adjust_priority_with_status')
|
|
if adjust:
|
|
return _('Adjust to Status')
|
|
|
|
priority = app.config.get_per('accounts', self.account, 'priority')
|
|
return str(priority)
|
|
|
|
|
|
class CutstomHostnameSetting(DialogSetting):
|
|
def __init__(self, *args, **kwargs):
|
|
DialogSetting.__init__(self, *args, **kwargs)
|
|
|
|
def get_setting_value(self):
|
|
custom = app.config.get_per('accounts', self.account, 'use_custom_host')
|
|
return _('On') if custom else _('Off')
|
|
|
|
|
|
class ChangePasswordSetting(DialogSetting):
|
|
def __init__(self, *args, **kwargs):
|
|
DialogSetting.__init__(self, *args, **kwargs)
|
|
|
|
def show_dialog(self, parent):
|
|
try:
|
|
self.change_dialog = ChangePasswordDialog(
|
|
self.account, self.on_changed, parent)
|
|
except GajimGeneralException:
|
|
return
|
|
self.change_dialog.set_modal(True)
|
|
|
|
def on_changed(self, new_password):
|
|
self.set_value(new_password)
|
|
|
|
def update_activatable(self, name, value):
|
|
activatable = False
|
|
if self.account in app.connections:
|
|
con = app.connections[self.account]
|
|
activatable = con.connected >= 2 and con.register_supported
|
|
self.set_activatable(activatable)
|