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/>.
from __future__ import annotations
from typing import Any
from typing import Optional
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.i18n import _
from gajim.common.i18n import Q_
from gajim.common.settings import AllSettingsT
from gajim.common import types
from .dialogs import DialogButton
from .dialogs import ConfirmationDialog
from .const import Setting
from .const import SettingKind
from .const import SettingType
from .settings import SettingsDialog
from .settings import SettingsBox

Daniel Brötzmann
committed
from .util import get_app_window
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._need_relogin: dict[str, list[AllSettingsT]] = {}
self._menu = AccountMenu()
self._settings = Settings()
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.add(self._menu)
box.add(self._settings)
self.add(box)
self._menu.connect('menu-activated', self._on_menu_activated)
self.connect('destroy', self._on_destroy)
self.connect_after('key-press-event', self._on_key_press)
def _on_menu_activated(self,
_listbox: Gtk.ListBox,
account: str,
name: str) -> None:
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)
def _on_key_press(self,
_widget: AccountsWindow,
event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
self.destroy()
def _on_destroy(self, _widget: AccountsWindow) -> None:
def update_account_label(self, account: str) -> None:
for account in self._accounts:
self._settings.update_proxy_list(account)
for account, r_settings in self._need_relogin.items():
active = app.settings.get_account_setting(account, 'active')
if not app.account_is_connected(account):
client = app.get_client(account)
client.disconnect(gracefully=True,
reconnect=True,
destroy_client=True)
_('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()
def _get_relogin_settings(account: str) -> list[AllSettingsT]:
values: list[AllSettingsT] = []
values.append(
app.settings.get_account_setting(account, 'client_cert'))
values.append(app.settings.get_account_setting(account, 'proxy'))
values.append(app.settings.get_account_setting(account, 'resource'))
values.append(
app.settings.get_account_setting(account, 'use_custom_host'))
values.append(app.settings.get_account_setting(account, 'custom_host'))
values.append(app.settings.get_account_setting(account, 'custom_port'))
def on_remove_account(account: str) -> None:
open_window('RemoveAccount', account=account)
def remove_account(self, account: str) -> None:
del self._need_relogin[account]
self._accounts[account].remove()
def add_account(self, account: str, initial: bool = False) -> None:
self._need_relogin[account] = self._get_relogin_settings(account)
self._accounts[account] = Account(account, self._menu, self._settings)
def select_account(self, account: str) -> None:
try:
self._accounts[account].select()
except KeyError:
log.warning('select_account() failed, account %s not found',
account)
def enable_account(self, account: str, state: bool) -> None:
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(vhomogeneous=False)
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: dict[GenericSettingPage, int] = {}
self._pages: dict[str, list[GenericSettingPage]] = defaultdict(list)
def add_page(self, page: GenericSettingPage) -> None:
self._stack.add_named(page, f'{page.account}-{page.name}')
self._page_signal_ids[page] = page.connect_signal(self._stack)
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]
for page in self._pages[account]:
if page.name != 'connection':
continue
page.update_proxy_entries()
__gsignals__ = {
'menu-activated': (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
}
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)
def _sort_func(row1: AccountRow, row2: AccountRow) -> int:
return locale.strcoll(row1.label.lower(), row2.label.lower())
self._accounts_listbox.add(row)
sub_menu = AccountSubMenu(row.account)
self._stack.add_named(sub_menu, f'{row.account}-menu')
sub_menu.connect('row-activated', self._on_sub_menu_row_activated)
if self._stack.get_visible_child_name() != 'accounts':
# activate 'back' button
listbox = cast(Gtk.ListBox, self._stack.get_visible_child())
back_row = cast(Gtk.ListBoxRow, listbox.get_row_at_index(1))
back_row.emit('activate')
sub_menu = self._stack.get_child_by_name(f'{row.account}-menu')
self._stack.remove(sub_menu)
row.destroy()
sub_menu.destroy()
def _on_account_row_activated(self,
_listbox: Gtk.ListBox,
row: AccountRow
) -> None:
self._stack.set_visible_child_name(f'{row.account}-menu')
listbox = cast(Gtk.ListBox, self._stack.get_visible_child())
listbox_row = cast(Gtk.ListBoxRow, listbox.get_row_at_index(2))
listbox_row.emit('activate')
def _on_sub_menu_row_activated(self,
listbox: AccountSubMenu,
row: MenuItem) -> None:
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,
def update_account_label(self, account: str) -> None:
sub_menu = cast(
AccountSubMenu, self._stack.get_child_by_name(f'{account}-menu'))
class AccountSubMenu(Gtk.ListBox):
__gsignals__ = {
'update': (GObject.SignalFlags.RUN_FIRST, None, (str,))
}
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')))
self.add(PageMenuItem('privacy', _('Privacy')))
self.add(PageMenuItem('connection', _('Connection')))
self.add(PageMenuItem('advanced', _('Advanced')))
self.add(RemoveMenuItem())
Gtk.ListBoxRow.__init__(self)
self._name = name
self._box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
spacing=12)
self._label = Gtk.Label()
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)
def __init__(self, parent: AccountSubMenu, account: str) -> None:
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: Gtk.ListBox,
account: str
) -> None:
account_label = app.get_account_label(account)
self._label.set_text(account_label)
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):
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'
elif name == 'advanced':
icon = 'preferences-other-symbolic'
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: str,
menu: AccountMenu,
settings: Settings
) -> None:
self._account = account
self._menu = menu
self._settings = settings
self._settings.add_page(GeneralPage(account))
self._settings.add_page(ConnectionPage(account))
self._settings.add_page(PrivacyPage(account))
self._settings.add_page(AdvancedPage(account))
self._account_row = AccountRow(account)
self._menu.add_account(self._account_row)
self._menu.show_all()
self._settings.show_all()
self.select()
self._menu.remove_account(self._account_row)
self._settings.remove_account(self._account)
self._account_row.update_account_label()
self._menu.update_account_label(self._account)
self._account_row.enable_account(state)
return self._account
class AccountRow(Gtk.ListBoxRow):
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')
account_enabled = app.settings.get_account_setting(
self._account, 'active')
self._switch = Gtk.Switch()
self._switch.set_active(account_enabled)
self._switch.set_vexpand(False)
self._switch_state_label = Gtk.Label()
self._switch_state_label.set_xalign(1)
self._switch_state_label.set_valign(Gtk.Align.CENTER)
self._set_label(account_enabled)
self._switch.connect(
'state-set', self._on_enable_switch, self._account)
box.add(self._switch)
box.add(self._switch_state_label)
box.add(Gtk.Separator())
box.add(self._label)
box.add(next_icon)
self.add(box)
@property
self._label.set_text(app.get_account_label(self._account))
self._switch.set_state(state)
text = Q_('?switch:On') if active else Q_('?switch:Off')
self._switch_state_label.set_text(text)
def _on_enable_switch(self,
switch: Gtk.Switch,
state: bool,
account: str
) -> int:
def _on_disconnect(event: AccountDisonnected) -> None:
if event.account != account:
return
app.ged.remove_event_handler('account-disconnected',
ged.CORE,
_on_disconnect)
app.interface.disable_account(account)
app.ged.register_event_handler('account-disconnected',
ged.CORE,
_on_disconnect)
client = app.get_client(account)
client.change_status('offline', 'offline')
switch.set_state(state)
old_state = app.settings.get_account_setting(account, 'active')
not app.connections[account].state.is_disconnected):
# Connecting or connected

Daniel Brötzmann
committed
window = get_app_window('AccountsWindow')
assert window is not None
_('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,
spacing=18)
self.set_vexpand(True)
self.set_hexpand(True)
self.set_margin_top(24)
pixbuf = Gtk.IconTheme.load_icon_for_scale(
Gtk.IconTheme.get_default(),
'org.gajim.Gajim-symbolic',
100,
self.get_scale_factor(),
self.add(Gtk.Image.new_from_pixbuf(pixbuf))
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)
self.add(button)
name = ''
def __init__(self, account: str, settings: list[Setting]) -> None:
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()
return stack.connect('notify::visible-child',
self._on_visible_child_changed)
def _on_visible_child_changed(self, stack: Gtk.Stack, _param: Any) -> None:
if self == stack.get_visible_child():
self.listbox.update_states()
class GeneralPage(GenericSettingPage):
name = 'general'
Setting(SettingKind.ENTRY,
_('Label'),
SettingType.ACCOUNT_CONFIG,
'account_label',
Setting(SettingKind.COLOR,
_('Color'),
SettingType.ACCOUNT_CONFIG,
'account_color',
desc=_('Recognize your account by color')),
Setting(SettingKind.LOGIN,
_('Login'),
SettingType.DIALOG,
desc=_('Change your account’s password, etc.'),
bind='account::anonymous_auth',
Setting(SettingKind.ACTION,
_('Import Contacts'),
SettingType.ACTION,
'-import-contacts',
# 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,
_('Global Status'),
SettingType.ACCOUNT_CONFIG,
'sync_with_global_status',
desc=_('Synchronise the status of all accounts')),
Setting(SettingKind.SWITCH,
_('Remember Last Status'),
SettingType.ACCOUNT_CONFIG,
'restore_last_status',
desc=_('Restore status and status message of your '
'last session')),
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: Any) -> None:

Daniel Brötzmann
committed
window = get_app_window('AccountsWindow')
assert window is not None
self._client: Optional[types.Client] = None
if app.account_is_connected(account):
self._client = app.get_client(account)
history_max_age = {
-1: _('Forever'),
86400: _('1 Day'),
604800: _('1 Week'),
2629743: _('1 Month'),
7889229: _('3 Months'),
15778458: _('6 Months'),
31556926: _('1 Year'),
}
chatstate_entries = {
'all': _('Enabled'),
'composing_only': _('Composing Only'),
'disabled': _('Disabled'),
}
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')),
_('Operating System'),
SettingType.ACCOUNT_CONFIG,
'send_os_info',
desc=_('Disclose information about the '
'operating system you currently use')),
Setting(SettingKind.SWITCH,
_('Media Playback'),
SettingType.ACCOUNT_CONFIG,
'publish_tune',
desc=_('Disclose information about media that is '
'currently being played on your system.')),
Setting(SettingKind.SWITCH,
_('Ignore Unknown Contacts'),
SettingType.ACCOUNT_CONFIG,
'ignore_unknown_contacts',
desc=_('Ignore everything from contacts not in your '
Setting(SettingKind.SWITCH,
_('Send Message Receipts'),
SettingType.ACCOUNT_CONFIG,
'answer_receipts',
desc=_('Tell your contacts if you received a message')),
Setting(SettingKind.POPOVER,
_('Send Chatstate'),
SettingType.ACCOUNT_CONFIG,
'send_chatstate_default',
desc=_('Default for chats'),
props={'entries': chatstate_entries,
'button-tooltip': _('Reset all chats to the '
'current default value'),
'button-style': 'destructive-action',
'button-callback': self._reset_send_chatstate}),
Setting(SettingKind.POPOVER,
_('Send Chatstate in Group Chats'),
SettingType.ACCOUNT_CONFIG,
'gc_send_chatstate_default',
desc=_('Default for group chats'),
props={'entries': chatstate_entries,
'button-tooltip': _('Reset all group chats to the '
'current default value'),
'button-style': 'destructive-action',
'button-callback': self._reset_gc_send_chatstate}),
Setting(SettingKind.SWITCH,
_('Send Read Markers'),
SettingType.VALUE,
app.settings.get_account_setting(
account, 'send_marker_default'),
callback=self._send_read_marker,
desc=_('Default for chats and private group chats'),
props={'button-text': _('Reset'),
'button-tooltip': _('Reset all chats to the '
'current default value'),
'button-style': 'destructive-action',
'button-callback': self._reset_send_read_marker}),
Setting(SettingKind.POPOVER,
_('Keep Chat History'),
SettingType.ACCOUNT_CONFIG,
'chat_history_max_age',
props={'entries': history_max_age},
desc=_('How long Gajim should keep your chat history')),
Setting(SettingKind.ACTION,
_('Export Chat History'),
SettingType.ACTION,
'-export-history',
props={'account': account},
desc=_('Export your chat history from Gajim'))
GenericSettingPage.__init__(self, account, settings)
def _reset_send_chatstate(button: Gtk.Button) -> None:
button.set_sensitive(False)
app.settings.set_contact_settings('send_chatstate', None)
@staticmethod
def _reset_gc_send_chatstate(button: Gtk.Button) -> None:
button.set_sensitive(False)
app.settings.set_group_chat_settings('send_chatstate', None)
def _send_idle_time(self, state: bool, _data: Any) -> None:
if self._client is not None:
self._client.get_module('LastActivity').set_enabled(state)
def _send_time_info(self, state: bool, _data: Any) -> None:
if self._client is not None:
self._client.get_module('EntityTime').set_enabled(state)
def _send_os_info(self, state: bool, _data: Any) -> None:
if self._client is not None:
self._client.get_module('SoftwareVersion').set_enabled(state)
def _publish_tune(self, state: bool, _data: Any) -> None:
if self._client is not None:
self._client.get_module('UserTune').set_enabled(state)
def _send_read_marker(self, state: bool, _data: Any) -> None:
app.settings.set_account_setting(
self._account, 'send_marker_default', state)
app.settings.set_account_setting(
self._account, 'gc_send_marker_private_default', state)
def _reset_send_read_marker(self, button: Gtk.Button) -> None:
button.set_sensitive(False)
app.settings.set_contact_settings('send_marker', None)
app.settings.set_group_chat_settings(
'send_marker', None, context='private')
for ctrl in app.window.get_controls(account=self._account):
Setting(SettingKind.POPOVER,
_('Proxy'),
SettingType.ACCOUNT_CONFIG,
'proxy',
name='proxy',
props={'entries': self._get_proxies(),
'button-icon-name': 'preferences-system-symbolic',
'button-callback': self._on_proxy_edit}),
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}),
Setting(SettingKind.SWITCH,
_('Use Unencrypted Connection'),
SettingType.ACCOUNT_CONFIG,
'use_plain_connection',
desc=_('Use an unencrypted connection to the server')),
Setting(SettingKind.SWITCH,
_('Confirm Unencrypted Connection'),
SettingType.ACCOUNT_CONFIG,
'confirm_unencrypted_connection',
desc=_('Show a confirmation dialog before connecting '
'unencrypted')),
GenericSettingPage.__init__(self, account, settings)
return {proxy: proxy for proxy in app.settings.get_proxies()}
@staticmethod
open_window('ManageProxies')
def update_proxy_entries(self) -> None:
popover_row = cast(PopoverSetting, self.listbox.get_setting('proxy'))
popover_row.update_entries(self._get_proxies())
class AdvancedPage(GenericSettingPage):
Setting(SettingKind.SWITCH,
_('Contact Information'),
SettingType.ACCOUNT_CONFIG,
'request_user_data',
desc=_('Request contact information (Tune, Location)')),
Setting(SettingKind.SWITCH,
_('Accept all Contact Requests'),
SettingType.ACCOUNT_CONFIG,
'autoauth',
desc=_('Automatically accept all contact requests')),
Setting(SettingKind.POPOVER,
_('Filetransfer Preference'),
SettingType.ACCOUNT_CONFIG,
'filetransfer_preference',
props={'entries': {'httpupload': _('Upload Files'),
'jingle': _('Send Files Directly')}},
desc=_('Preferred file transfer mechanism for '
'file drag&drop on a chat window')),
Setting(SettingKind.SWITCH,
_('Security Labels'),
SettingType.ACCOUNT_CONFIG,
'enable_security_labels',
desc=_('Show labels describing confidentiality of '
'messages, if the server supports XEP-0258'))
]
GenericSettingPage.__init__(self, account, settings)
def __init__(self, account: str, parent: Gtk.Window) -> None:
neg_priority = app.settings.get('enable_negative_priority')
if neg_priority:
range_ = (-128, 127)
else:
range_ = (0, 127)
Setting(SettingKind.SWITCH,
_('Adjust to status'),
SettingType.ACCOUNT_CONFIG,
'adjust_priority_with_status'),
Setting(SettingKind.SPIN,
_('Priority'),
SettingType.ACCOUNT_CONFIG,
'priority',
bind='account::adjust_priority_with_status',
inverted=True,
props={'range_': range_}),