Newer
Older
# 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 sys
import locale
import logging
from collections import defaultdict
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from gi.repository import GObject
from gajim.common import app
from gajim.common import passwords
from gajim.common.i18n import _
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.gtk.settings import SettingsBox
from gajim.gtk.util import open_window
log = logging.getLogger('gajim.gtk.accounts')
class AccountsWindow(Gtk.ApplicationWindow):
def __init__(self):
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_show_menubar(False)
self.set_name('AccountsWindow')
self._menu = AccountMenu()
self._settings = Settings()
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.add(self._menu)
box.add(self._settings)
self.add(box)
if account == 'Local':
# Disable zeroconf support until its working again
continue
self._menu.connect('menu-activated', self._on_menu_activated)
self.connect('destroy', self._on_destroy)
self.connect('key-press-event', self._on_key_press)
def _on_menu_activated(self, _listbox, account, name):
if name == 'back':
self._settings.set_page('add-account')
self._check_relogin()
elif name == 'remove':
self.on_remove_account(account)
else:
self._settings.set_page(name)
if event.keyval == Gdk.KEY_Escape:
self.destroy()
def _on_destroy(self, *args):
self._check_relogin()
def update_account_label(self, account):
self._accounts[account].update_account_label()
for account in self._accounts:
self._settings.update_proxy_list(account)
def _check_relogin(self):
for account in self._need_relogin:
settings = self._get_relogin_settings(account)
active = app.config.get_per('accounts', account, 'active')
if settings != self._need_relogin[account]:
self._need_relogin[account] = settings
if not app.account_is_connected(account):
return
if account == app.ZEROCONF_ACC_NAME:
app.connections[app.ZEROCONF_ACC_NAME].update_details()
return
def relog():
app.connections[account].disconnect(gracefully=True,
reconnect=True,
destroy_client=True)
NewConfirmationDialog(
_('Re-Login'),
_('Re-Login now?'),
_('To apply all changes instantly, you have to re-login.'),
[DialogButton.make('Cancel',
text=_('_Later')),
DialogButton.make('Accept',
text=_('_Re-Login'),
transient_for=self).show()
settings = ['zeroconf_first_name', 'zeroconf_last_name',
'zeroconf_jabber_id', 'zeroconf_email']
settings = ['client_cert', 'proxy', 'resource',
'use_custom_host', 'custom_host', 'custom_port']
for setting in settings:
values.append(app.config.get_per('accounts', account, setting))
@staticmethod
def on_remove_account(account):
app.interface.raise_dialog('unread-events-on-remove-account')
return
if app.config.get_per('accounts', account, 'is_zeroconf'):
# Should never happen as button is insensitive
return
open_window('RemoveAccount', account=account)
del self._need_relogin[account]
self._accounts[account].remove()
self._need_relogin[account] = self._get_relogin_settings(account)
self._accounts[account] = Account(account, self._menu, self._settings)
try:
self._accounts[account].select()
except KeyError:
log.warning('select_account() failed, account %s not found',
account)
def enable_account(self, account, state):
self._accounts[account].enable_account(state)
Gtk.ScrolledWindow.__init__(self)
self.set_hexpand(True)
self.set_vexpand(True)
self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self._stack = Gtk.Stack()
self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
self._stack.add_named(AddNewAccountPage(), 'add-account')
self.get_style_context().add_class('accounts-settings')
self.add(self._stack)
self._page_signal_ids = {}
self._pages = defaultdict(list)
def add_page(self, page):
self._pages[page.account].append(page)
self._stack.add_named(page, '%s-%s' % (page.account, page.name))
self._page_signal_ids[page] = page.connect_signal(self._stack)
def set_page(self, name):
self._stack.set_visible_child_name(name)
def remove_account(self, account):
for page in self._pages[account]:
signal_id = self._page_signal_ids[page]
del self._page_signal_ids[page]
self._stack.disconnect(signal_id)
self._stack.remove(page)
page.destroy()
del self._pages[account]
def update_proxy_list(self, account):
for page in self._pages[account]:
if page.name != 'connection':
continue
page.listbox.get_setting('proxy').update_values()
__gsignals__ = {
'menu-activated': (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
}
def __init__(self):
Gtk.Box.__init__(self)
self.set_hexpand(False)
self.set_size_request(160, -1)
self.get_style_context().add_class('accounts-menu')
self._stack = Gtk.Stack()
self._stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
self._accounts_listbox = Gtk.ListBox()
self._accounts_listbox.set_sort_func(self._sort_func)
self._accounts_listbox.get_style_context().add_class('settings-box')
self._accounts_listbox.connect('row-activated',
self._on_account_row_activated)
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scrolled.add(self._accounts_listbox)
self._stack.add_named(scrolled, 'accounts')
self.add(self._stack)
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
@staticmethod
def _sort_func(row1, row2):
if row1.label == 'Local':
return -1
return locale.strcoll(row1.label.lower(), row2.label.lower())
def add_account(self, row):
self._accounts_listbox.add(row)
sub_menu = AccountSubMenu(row.account)
self._stack.add_named(sub_menu, '%s-menu' % row.account)
sub_menu.connect('row-activated', self._on_sub_menu_row_activated)
def remove_account(self, row):
if self._stack.get_visible_child_name() != 'accounts':
# activate 'back' button
self._stack.get_visible_child().get_row_at_index(1).emit('activate')
self._accounts_listbox.remove(row)
sub_menu = self._stack.get_child_by_name('%s-menu' % row.account)
self._stack.remove(sub_menu)
row.destroy()
sub_menu.destroy()
def _on_account_row_activated(self, _listbox, row):
self._stack.set_visible_child_name('%s-menu' % row.account)
self._stack.get_visible_child().get_row_at_index(2).emit('activate')
def _on_sub_menu_row_activated(self, listbox, row):
if row.name == 'back':
self._stack.set_visible_child_full(
'accounts', Gtk.StackTransitionType.OVER_RIGHT)
if row.name in ('back', 'remove'):
self.emit('menu-activated', listbox.account, row.name)
else:
self.emit('menu-activated',
listbox.account,
'%s-%s' % (listbox.account, row.name))
def update_account_label(self, account):
self._accounts_listbox.invalidate_sort()
sub_menu = self._stack.get_child_by_name('%s-menu' % account)
sub_menu.update()
class AccountSubMenu(Gtk.ListBox):
__gsignals__ = {
'update': (GObject.SignalFlags.RUN_FIRST, None, (str,))
}
def __init__(self, account):
Gtk.ListBox.__init__(self)
self.set_vexpand(True)
self.set_hexpand(True)
self.get_style_context().add_class('settings-box')
self._account = account
self.add(AccountLabelMenuItem(self, self._account))
self.add(BackMenuItem())
self.add(PageMenuItem('general', _('General')))
if account != 'Local':
self.add(PageMenuItem('privacy', _('Privacy')))
self.add(PageMenuItem('connection', _('Connection')))
self.add(RemoveMenuItem())
@property
def account(self):
return self._account
class MenuItem(Gtk.ListBoxRow):
def __init__(self, name):
Gtk.ListBoxRow.__init__(self)
self._name = name
self._box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
spacing=12)
self._label = Gtk.Label()
@property
def name(self):
return self._name
class RemoveMenuItem(MenuItem):
def __init__(self):
super().__init__('remove')
self._label.set_text(_('Remove'))
image = Gtk.Image.new_from_icon_name('user-trash-symbolic',
Gtk.IconSize.MENU)
self.set_selectable(False)
image.get_style_context().add_class('error-color')
self._box.add(image)
self._box.add(self._label)
class AccountLabelMenuItem(MenuItem):
def __init__(self, parent, account):
super().__init__('account-label')
self._update_account_label(parent, account)
self.set_selectable(False)
self.set_sensitive(False)
self.set_activatable(False)
image = Gtk.Image.new_from_icon_name('avatar-default-symbolic',
Gtk.IconSize.MENU)
image.get_style_context().add_class('insensitive-fg-color')
self._label.get_style_context().add_class('accounts-label-row')
self._label.set_ellipsize(Pango.EllipsizeMode.END)
self._label.set_xalign(0)
self._box.add(image)
self._box.add(self._label)
parent.connect('update', self._update_account_label)
def _update_account_label(self, _listbox, account):
account_label = app.get_account_label(account)
self._label.set_text(account_label)
class BackMenuItem(MenuItem):
def __init__(self):
super().__init__('back')
self.set_selectable(False)
image = Gtk.Image.new_from_icon_name('go-previous-symbolic',
Gtk.IconSize.MENU)
image.get_style_context().add_class('insensitive-fg-color')
self._box.add(image)
self._box.add(self._label)
class PageMenuItem(MenuItem):
def __init__(self, name, label):
super().__init__(name)
if name == 'general':
icon = 'preferences-system-symbolic'
elif name == 'privacy':
icon = 'preferences-system-privacy-symbolic'
elif name == 'connection':
icon = 'preferences-system-network-symbolic'
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU)
self._label.set_text(label)
self._box.add(image)
self._box.add(self._label)
class Account:
def __init__(self, account, menu, settings):
self._account = account
self._menu = menu
self._settings = settings
if account == app.ZEROCONF_ACC_NAME:
self._settings.add_page(ZeroConfPage(account))
else:
self._settings.add_page(GeneralPage(account))
self._settings.add_page(ConnectionPage(account))
self._settings.add_page(PrivacyPage(account))
self._account_row = AccountRow(account)
self._menu.add_account(self._account_row)
def select(self):
self._account_row.emit('activate')
def show(self):
self._menu.show_all()
self._settings.show_all()
self.select()
def remove(self):
self._menu.remove_account(self._account_row)
self._settings.remove_account(self._account)
def update_account_label(self):
self._account_row.update_account_label()
self._menu.update_account_label(self._account)
def enable_account(self, state):
self._account_row.enable_account(state)
@property
def menu(self):
return self._menu
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
@property
def account(self):
return self._account
@property
def settings(self):
return self._account
class AccountRow(Gtk.ListBoxRow):
def __init__(self, account):
Gtk.ListBoxRow.__init__(self)
self.set_selectable(False)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
self._account = account
self._label = Gtk.Label(label=app.get_account_label(account))
self._label.set_halign(Gtk.Align.START)
self._label.set_hexpand(True)
self._label.set_ellipsize(Pango.EllipsizeMode.END)
self._label.set_xalign(0)
self._label.set_width_chars(18)
next_icon = Gtk.Image.new_from_icon_name('go-next-symbolic',
Gtk.IconSize.MENU)
next_icon.get_style_context().add_class('insensitive-fg-color')
self._switch = Gtk.Switch()
self._switch.set_active(
app.config.get_per('accounts', self._account, 'active'))
self._switch.set_vexpand(False)
if (self._account == app.ZEROCONF_ACC_NAME and
not app.is_installed('ZEROCONF')):
self._switch.set_active(False)
self.set_activatable(False)
self.set_sensitive(False)
if sys.platform in ('win32', 'darwin'):
tooltip = _('Please check if Bonjour is installed.')
else:
tooltip = _('Please check if Avahi is installed.')
self.set_tooltip_text(tooltip)
self._switch.connect(
'state-set', self._on_enable_switch, self._account)
box.add(self._switch)
box.add(self._label)
box.add(next_icon)
self.add(box)
@property
def account(self):
return self._account
@property
def label(self):
return self._label.get_text()
def update_account_label(self):
self._label.set_text(app.get_account_label(self._account))
def enable_account(self, state):
self._switch.set_state(state)
def _on_enable_switch(self, switch, state, account):
def _disable():
app.connections[account].change_status('offline', 'offline')
app.interface.disable_account(account)
switch.set_state(state)
old_state = app.config.get_per('accounts', account, 'active')
if old_state == state:
not app.connections[account].state.is_disconnected):
# Connecting or connected
NewConfirmationDialog(
_('Disable Account'),
_('Account %s is still connected') % account,
_('All chat and group chat windows will be closed.'),
[DialogButton.make('Cancel',
callback=lambda: switch.set_active(True)),
DialogButton.make('Remove',
callback=_disable)],
return Gdk.EVENT_STOP
app.interface.enable_account(account)
app.interface.disable_account(account)
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.set_vexpand(True)
self.set_hexpand(True)
button = Gtk.Button(label=_('Add Account'))
button.get_style_context().add_class('suggested-action')
button.set_action_name('app.add-account')
button.set_halign(Gtk.Align.CENTER)
button.set_valign(Gtk.Align.CENTER)
self.add(button)
class GenericSettingPage(Gtk.Box):
def __init__(self, account, settings):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.set_valign(Gtk.Align.START)
self.set_vexpand(True)
self.account = account
self.listbox = SettingsBox(account)
self.listbox.get_style_context().add_class('accounts-settings-border')
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
self.listbox.set_vexpand(False)
self.listbox.set_valign(Gtk.Align.END)
for setting in settings:
self.listbox.add_setting(setting)
self.listbox.update_states()
self.listbox.connect('row-activated', self.on_row_activated)
def connect_signal(self, stack):
return stack.connect('notify::visible-child',
self._on_visible_child_changed)
def _on_visible_child_changed(self, stack, _param):
if self == stack.get_visible_child():
self.listbox.update_states()
@staticmethod
def on_row_activated(_listbox, row):
row.on_row_activated()
class GeneralPage(GenericSettingPage):
name = 'general'
def __init__(self, account):
settings = [
Setting(SettingKind.ENTRY, _('Label'),
SettingType.ACCOUNT_CONFIG, 'account_label',
callback=self._on_account_name_change),
Setting(SettingKind.COLOR, _('Color'),
SettingType.ACCOUNT_CONFIG, 'account_color',
desc=_('Recognize your account by color')),
Setting(SettingKind.LOGIN, _('Login'), SettingType.DIALOG,
props={'dialog': LoginDialog}),
Setting(SettingKind.ACTION, _('Import Contacts'),
SettingType.ACTION, '-import-contacts',
props={'account': account}),
# Currently not supported by nbxmpp
#
# Setting(SettingKind.DIALOG, _('Client Certificate'),
# SettingType.DIALOG, props={'dialog': CertificateDialog}),
Setting(SettingKind.SWITCH, _('Connect on startup'),
SettingType.ACCOUNT_CONFIG, 'autoconnect'),
Setting(SettingKind.SWITCH,
_('Save conversations for all contacts'),
SettingType.ACCOUNT_CONFIG, 'no_log_for',
desc=_('Store conversations on the harddrive')),
Setting(SettingKind.SWITCH, _('Global Status'),
SettingType.ACCOUNT_CONFIG, 'sync_with_global_status',
desc=_('Synchronise the status of all accounts')),
Setting(SettingKind.SWITCH, _('Use file transfer proxies'),
SettingType.ACCOUNT_CONFIG, 'use_ft_proxies'),
]
GenericSettingPage.__init__(self, account, settings)
def _on_account_name_change(self, *args):
self.get_toplevel().update_account_label(self.account)
name = 'privacy'
def __init__(self, account):
settings = [
Setting(SettingKind.SWITCH, _('Idle Time'),
SettingType.ACCOUNT_CONFIG, 'send_idle_time',
desc=_('Disclose the time of your last activity')),
Setting(SettingKind.SWITCH, _('Local System Time'),
SettingType.ACCOUNT_CONFIG, 'send_time_info',
desc=_('Disclose the local system time of the '
'device Gajim runs on')),
Setting(SettingKind.SWITCH, _('Client / Operating System'),
SettingType.ACCOUNT_CONFIG, 'send_os_info',
'and operating system you currently use')),
Setting(SettingKind.SWITCH, _('Ignore Unknown Contacts'),
SettingType.ACCOUNT_CONFIG, 'ignore_unknown_contacts',
desc=_('Ignore everything from contacts not in your '
'Roster')),
]
GenericSettingPage.__init__(self, account, settings)
name = 'connection'
def __init__(self, account):
settings = [
Setting(SettingKind.PROXY, _('Proxy'),
SettingType.ACCOUNT_CONFIG, 'proxy', name='proxy'),
Setting(SettingKind.HOSTNAME, _('Hostname'), SettingType.DIALOG,
desc=_('Manually set the hostname for the server'),
props={'dialog': CutstomHostnameDialog}),
Setting(SettingKind.ENTRY, _('Resource'),
SettingType.ACCOUNT_CONFIG, 'resource'),
Setting(SettingKind.PRIORITY, _('Priority'),
SettingType.DIALOG, props={'dialog': PriorityDialog}),
GenericSettingPage.__init__(self, account, settings)
name = 'general'
def __init__(self, account):
settings = [
Setting(SettingKind.DIALOG, _('Profile'),
SettingType.DIALOG,
props={'dialog': ZeroconfProfileDialog}),
Setting(SettingKind.SWITCH, _('Connect on startup'),
SettingType.ACCOUNT_CONFIG, 'autoconnect',
desc=_('Use environment variable')),
Setting(SettingKind.SWITCH,
_('Save conversations for all contacts'),
SettingType.ACCOUNT_CONFIG, 'no_log_for',
desc=_('Store conversations on the harddrive')),
Setting(SettingKind.SWITCH, _('Global Status'),
SettingType.ACCOUNT_CONFIG, 'sync_with_global_status',
desc=_('Synchronize the status of all accounts')),
GenericSettingPage.__init__(self, account, settings)
settings = [
Setting(SettingKind.ENTRY, _('First Name'),
SettingType.ACCOUNT_CONFIG, 'zeroconf_first_name'),
Setting(SettingKind.ENTRY, _('Last Name'),
SettingType.ACCOUNT_CONFIG, 'zeroconf_last_name'),
Setting(SettingKind.ENTRY, _('XMPP Address'),
SettingType.ACCOUNT_CONFIG, 'zeroconf_jabber_id'),
Setting(SettingKind.ENTRY, _('Email'),
SettingType.ACCOUNT_CONFIG, 'zeroconf_email'),
SettingsDialog.__init__(self, parent, _('Profile'),
def __init__(self, account, parent):
neg_priority = app.config.get('enable_negative_priority')
if neg_priority:
range_ = (-128, 127)
else:
range_ = (0, 127)
settings = [
Setting(SettingKind.SWITCH, _('Adjust to status'),
SettingType.ACCOUNT_CONFIG, 'adjust_priority_with_status',
'adjust'),
Setting(SettingKind.SPIN, _('Priority'),
SettingType.ACCOUNT_CONFIG, 'priority',
enabledif=('adjust', False), props={'range_': range_}),
SettingsDialog.__init__(self, parent, _('Priority'),
self.connect('destroy', self.on_destroy)
def on_destroy(self, *args):
# Update priority
if self.account not in app.connections:
return
show = app.connections[self.account].status
status = app.connections[self.account].status_message
app.connections[self.account].change_status(show, status)
type_values = ('START TLS', 'DIRECT TLS', 'PLAIN')
settings = [
Setting(SettingKind.SWITCH, _('Enable'),
SettingType.ACCOUNT_CONFIG,
'use_custom_host', name='custom'),
Setting(SettingKind.ENTRY, _('Hostname'),
SettingType.ACCOUNT_CONFIG, 'custom_host',
enabledif=('custom', True)),
Setting(SettingKind.ENTRY, _('Port'),
SettingType.ACCOUNT_CONFIG, 'custom_port',
enabledif=('custom', True)),
Setting(SettingKind.COMBO, _('Type'),
SettingType.ACCOUNT_CONFIG, 'custom_type',
enabledif=('custom', True),
props={'combo_items': type_values}),
SettingsDialog.__init__(self, parent, _('Connection Settings'),
Setting(SettingKind.FILECHOOSER, _('Client Certificate'),
SettingType.ACCOUNT_CONFIG, 'client_cert',
props={'filefilter': (_('PKCS12 Files'), '*.p12')}),
Setting(SettingKind.SWITCH, _('Encrypted Certificate'),
SettingType.ACCOUNT_CONFIG, 'client_cert_encrypted'),
SettingsDialog.__init__(self, parent, _('Certificate Settings'),
settings = [
Setting(SettingKind.ENTRY, _('Password'),
SettingType.ACCOUNT_CONFIG, 'password', name='password',
enabledif=('savepass', True)),
Setting(SettingKind.SWITCH, _('Save Password'),
SettingType.ACCOUNT_CONFIG, 'savepass', name='savepass'),
Setting(SettingKind.CHANGEPASSWORD, _('Change Password'),
SettingType.DIALOG, callback=self.on_password_change,
props={'dialog': None}),
SettingsDialog.__init__(self, parent, _('Login Settings'),
def on_password_change(self, new_password, _data):
passwords.save_password(self.account, new_password)
def on_destroy(self, *args):
savepass = app.config.get_per('accounts', self.account, 'savepass')
if not savepass: