1# This file is part of Gajim. 2# 3# Gajim is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published 5# by the Free Software Foundation; version 3 only. 6# 7# Gajim is distributed in the hope that it will be useful, 8# but WITHOUT ANY WARRANTY; without even the implied warranty of 9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10# GNU General Public License for more details. 11# 12# You should have received a copy of the GNU General Public License 13# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 14 15import sys 16import locale 17import logging 18from collections import defaultdict 19 20from gi.repository import Gtk 21from gi.repository import Gdk 22from gi.repository import Pango 23from gi.repository import GObject 24 25from gajim.common import app 26from gajim.common import passwords 27from gajim.common.i18n import _ 28from gajim.common.i18n import Q_ 29 30from .dialogs import DialogButton 31from .dialogs import ConfirmationDialog 32from .const import Setting 33from .const import SettingKind 34from .const import SettingType 35from .settings import SettingsDialog 36from .settings import SettingsBox 37from .util import open_window 38 39 40log = logging.getLogger('gajim.gui.accounts') 41 42 43class AccountsWindow(Gtk.ApplicationWindow): 44 def __init__(self): 45 Gtk.ApplicationWindow.__init__(self) 46 self.set_application(app.app) 47 self.set_position(Gtk.WindowPosition.CENTER) 48 self.set_show_menubar(False) 49 self.set_name('AccountsWindow') 50 self.set_default_size(700, 550) 51 self.set_resizable(True) 52 self.set_title(_('Accounts')) 53 self._need_relogin = {} 54 self._accounts = {} 55 56 self._menu = AccountMenu() 57 self._settings = Settings() 58 59 box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 60 box.add(self._menu) 61 box.add(self._settings) 62 self.add(box) 63 64 for account in app.get_accounts_sorted(): 65 if account == 'Local': 66 # Disable zeroconf support until its working again 67 continue 68 self.add_account(account, initial=True) 69 70 self._menu.connect('menu-activated', self._on_menu_activated) 71 self.connect('destroy', self._on_destroy) 72 self.connect_after('key-press-event', self._on_key_press) 73 74 self.show_all() 75 76 def _on_menu_activated(self, _listbox, account, name): 77 if name == 'back': 78 self._settings.set_page('add-account') 79 self._check_relogin() 80 elif name == 'remove': 81 self.on_remove_account(account) 82 else: 83 self._settings.set_page(name) 84 85 def _on_key_press(self, _widget, event): 86 if event.keyval == Gdk.KEY_Escape: 87 self.destroy() 88 89 def _on_destroy(self, *args): 90 self._check_relogin() 91 92 def update_account_label(self, account): 93 self._accounts[account].update_account_label() 94 95 def update_proxy_list(self): 96 for account in self._accounts: 97 self._settings.update_proxy_list(account) 98 99 def _check_relogin(self): 100 for account in self._need_relogin: 101 settings = self._get_relogin_settings(account) 102 active = app.settings.get_account_setting(account, 'active') 103 if settings != self._need_relogin[account]: 104 self._need_relogin[account] = settings 105 if active: 106 self._relog(account) 107 break 108 109 def _relog(self, account): 110 if not app.account_is_connected(account): 111 return 112 113 if account == app.ZEROCONF_ACC_NAME: 114 app.connections[app.ZEROCONF_ACC_NAME].update_details() 115 return 116 117 def relog(): 118 app.connections[account].disconnect(gracefully=True, 119 reconnect=True, 120 destroy_client=True) 121 122 ConfirmationDialog( 123 _('Re-Login'), 124 _('Re-Login now?'), 125 _('To apply all changes instantly, you have to re-login.'), 126 [DialogButton.make('Cancel', 127 text=_('_Later')), 128 DialogButton.make('Accept', 129 text=_('_Re-Login'), 130 callback=relog)], 131 transient_for=self).show() 132 133 @staticmethod 134 def _get_relogin_settings(account): 135 if account == app.ZEROCONF_ACC_NAME: 136 settings = ['zeroconf_first_name', 'zeroconf_last_name', 137 'zeroconf_jabber_id', 'zeroconf_email'] 138 else: 139 settings = ['client_cert', 'proxy', 'resource', 140 'use_custom_host', 'custom_host', 'custom_port'] 141 142 values = [] 143 for setting in settings: 144 values.append(app.settings.get_account_setting(account, setting)) 145 return values 146 147 @staticmethod 148 def on_remove_account(account): 149 if app.events.get_events(account): 150 app.interface.raise_dialog('unread-events-on-remove-account') 151 return 152 153 if app.settings.get_account_setting(account, 'is_zeroconf'): 154 # Should never happen as button is insensitive 155 return 156 157 open_window('RemoveAccount', account=account) 158 159 def remove_account(self, account): 160 del self._need_relogin[account] 161 self._accounts[account].remove() 162 163 def add_account(self, account, initial=False): 164 self._need_relogin[account] = self._get_relogin_settings(account) 165 self._accounts[account] = Account(account, self._menu, self._settings) 166 if not initial: 167 self._accounts[account].show() 168 169 def select_account(self, account): 170 try: 171 self._accounts[account].select() 172 except KeyError: 173 log.warning('select_account() failed, account %s not found', 174 account) 175 176 def enable_account(self, account, state): 177 self._accounts[account].enable_account(state) 178 179 180class Settings(Gtk.ScrolledWindow): 181 def __init__(self): 182 Gtk.ScrolledWindow.__init__(self) 183 self.set_hexpand(True) 184 self.set_vexpand(True) 185 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 186 self._stack = Gtk.Stack(vhomogeneous=False) 187 self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) 188 self._stack.add_named(AddNewAccountPage(), 'add-account') 189 self.get_style_context().add_class('accounts-settings') 190 191 self.add(self._stack) 192 self._page_signal_ids = {} 193 self._pages = defaultdict(list) 194 195 def add_page(self, page): 196 self._pages[page.account].append(page) 197 self._stack.add_named(page, '%s-%s' % (page.account, page.name)) 198 self._page_signal_ids[page] = page.connect_signal(self._stack) 199 200 def set_page(self, name): 201 self._stack.set_visible_child_name(name) 202 203 def remove_account(self, account): 204 for page in self._pages[account]: 205 signal_id = self._page_signal_ids[page] 206 del self._page_signal_ids[page] 207 self._stack.disconnect(signal_id) 208 self._stack.remove(page) 209 page.destroy() 210 del self._pages[account] 211 212 def update_proxy_list(self, account): 213 for page in self._pages[account]: 214 if page.name != 'connection': 215 continue 216 page.update_proxy_entries() 217 218 219class AccountMenu(Gtk.Box): 220 221 __gsignals__ = { 222 'menu-activated': (GObject.SignalFlags.RUN_FIRST, None, (str, str)), 223 } 224 225 def __init__(self): 226 Gtk.Box.__init__(self) 227 self.set_hexpand(False) 228 self.set_size_request(160, -1) 229 230 self.get_style_context().add_class('accounts-menu') 231 232 self._stack = Gtk.Stack() 233 self._stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT) 234 235 self._accounts_listbox = Gtk.ListBox() 236 self._accounts_listbox.set_sort_func(self._sort_func) 237 self._accounts_listbox.get_style_context().add_class('settings-box') 238 self._accounts_listbox.connect('row-activated', 239 self._on_account_row_activated) 240 241 scrolled = Gtk.ScrolledWindow() 242 scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 243 scrolled.add(self._accounts_listbox) 244 self._stack.add_named(scrolled, 'accounts') 245 self.add(self._stack) 246 247 @staticmethod 248 def _sort_func(row1, row2): 249 if row1.label == 'Local': 250 return -1 251 return locale.strcoll(row1.label.lower(), row2.label.lower()) 252 253 def add_account(self, row): 254 self._accounts_listbox.add(row) 255 sub_menu = AccountSubMenu(row.account) 256 self._stack.add_named(sub_menu, '%s-menu' % row.account) 257 258 sub_menu.connect('row-activated', self._on_sub_menu_row_activated) 259 260 def remove_account(self, row): 261 if self._stack.get_visible_child_name() != 'accounts': 262 # activate 'back' button 263 self._stack.get_visible_child().get_row_at_index(1).emit('activate') 264 self._accounts_listbox.remove(row) 265 sub_menu = self._stack.get_child_by_name('%s-menu' % row.account) 266 self._stack.remove(sub_menu) 267 row.destroy() 268 sub_menu.destroy() 269 270 def _on_account_row_activated(self, _listbox, row): 271 self._stack.set_visible_child_name('%s-menu' % row.account) 272 self._stack.get_visible_child().get_row_at_index(2).emit('activate') 273 274 def _on_sub_menu_row_activated(self, listbox, row): 275 if row.name == 'back': 276 self._stack.set_visible_child_full( 277 'accounts', Gtk.StackTransitionType.OVER_RIGHT) 278 279 if row.name in ('back', 'remove'): 280 self.emit('menu-activated', listbox.account, row.name) 281 282 else: 283 self.emit('menu-activated', 284 listbox.account, 285 '%s-%s' % (listbox.account, row.name)) 286 287 def update_account_label(self, account): 288 self._accounts_listbox.invalidate_sort() 289 sub_menu = self._stack.get_child_by_name('%s-menu' % account) 290 sub_menu.update() 291 292 293class AccountSubMenu(Gtk.ListBox): 294 295 __gsignals__ = { 296 'update': (GObject.SignalFlags.RUN_FIRST, None, (str,)) 297 } 298 299 def __init__(self, account): 300 Gtk.ListBox.__init__(self) 301 self.set_vexpand(True) 302 self.set_hexpand(True) 303 self.get_style_context().add_class('settings-box') 304 305 self._account = account 306 307 self.add(AccountLabelMenuItem(self, self._account)) 308 self.add(BackMenuItem()) 309 self.add(PageMenuItem('general', _('General'))) 310 if account != 'Local': 311 self.add(PageMenuItem('privacy', _('Privacy'))) 312 self.add(PageMenuItem('connection', _('Connection'))) 313 self.add(PageMenuItem('advanced', _('Advanced'))) 314 self.add(RemoveMenuItem()) 315 316 @property 317 def account(self): 318 return self._account 319 320 def update(self): 321 self.emit('update', self._account) 322 323 324class MenuItem(Gtk.ListBoxRow): 325 def __init__(self, name): 326 Gtk.ListBoxRow.__init__(self) 327 self._name = name 328 self._box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, 329 spacing=12) 330 self._label = Gtk.Label() 331 332 self.add(self._box) 333 334 @property 335 def name(self): 336 return self._name 337 338 339class RemoveMenuItem(MenuItem): 340 def __init__(self): 341 super().__init__('remove') 342 self._label.set_text(_('Remove')) 343 image = Gtk.Image.new_from_icon_name('user-trash-symbolic', 344 Gtk.IconSize.MENU) 345 346 self.set_selectable(False) 347 image.get_style_context().add_class('error-color') 348 349 self._box.add(image) 350 self._box.add(self._label) 351 352 353class AccountLabelMenuItem(MenuItem): 354 def __init__(self, parent, account): 355 super().__init__('account-label') 356 self._update_account_label(parent, account) 357 358 self.set_selectable(False) 359 self.set_sensitive(False) 360 self.set_activatable(False) 361 362 image = Gtk.Image.new_from_icon_name('avatar-default-symbolic', 363 Gtk.IconSize.MENU) 364 image.get_style_context().add_class('insensitive-fg-color') 365 366 self._label.get_style_context().add_class('accounts-label-row') 367 self._label.set_ellipsize(Pango.EllipsizeMode.END) 368 self._label.set_xalign(0) 369 370 self._box.add(image) 371 self._box.add(self._label) 372 373 parent.connect('update', self._update_account_label) 374 375 def _update_account_label(self, _listbox, account): 376 account_label = app.get_account_label(account) 377 self._label.set_text(account_label) 378 379 380class BackMenuItem(MenuItem): 381 def __init__(self): 382 super().__init__('back') 383 self.set_selectable(False) 384 385 self._label.set_text(_('Back')) 386 387 image = Gtk.Image.new_from_icon_name('go-previous-symbolic', 388 Gtk.IconSize.MENU) 389 image.get_style_context().add_class('insensitive-fg-color') 390 391 self._box.add(image) 392 self._box.add(self._label) 393 394 395class PageMenuItem(MenuItem): 396 def __init__(self, name, label): 397 super().__init__(name) 398 399 if name == 'general': 400 icon = 'preferences-system-symbolic' 401 elif name == 'privacy': 402 icon = 'preferences-system-privacy-symbolic' 403 elif name == 'connection': 404 icon = 'preferences-system-network-symbolic' 405 elif name == 'advanced': 406 icon = 'preferences-other-symbolic' 407 else: 408 icon = 'dialog-error-symbolic' 409 410 image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU) 411 self._label.set_text(label) 412 413 self._box.add(image) 414 self._box.add(self._label) 415 416 417class Account: 418 def __init__(self, account, menu, settings): 419 self._account = account 420 self._menu = menu 421 self._settings = settings 422 423 if account == app.ZEROCONF_ACC_NAME: 424 self._settings.add_page(ZeroConfPage(account)) 425 else: 426 self._settings.add_page(GeneralPage(account)) 427 self._settings.add_page(ConnectionPage(account)) 428 self._settings.add_page(PrivacyPage(account)) 429 self._settings.add_page(AdvancedPage(account)) 430 431 self._account_row = AccountRow(account) 432 self._menu.add_account(self._account_row) 433 434 def select(self): 435 self._account_row.emit('activate') 436 437 def show(self): 438 self._menu.show_all() 439 self._settings.show_all() 440 self.select() 441 442 def remove(self): 443 self._menu.remove_account(self._account_row) 444 self._settings.remove_account(self._account) 445 446 def update_account_label(self): 447 self._account_row.update_account_label() 448 self._menu.update_account_label(self._account) 449 450 def enable_account(self, state): 451 self._account_row.enable_account(state) 452 453 @property 454 def menu(self): 455 return self._menu 456 457 @property 458 def account(self): 459 return self._account 460 461 @property 462 def settings(self): 463 return self._account 464 465 466class AccountRow(Gtk.ListBoxRow): 467 def __init__(self, account): 468 Gtk.ListBoxRow.__init__(self) 469 self.set_selectable(False) 470 471 box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) 472 473 self._account = account 474 475 self._label = Gtk.Label(label=app.get_account_label(account)) 476 self._label.set_halign(Gtk.Align.START) 477 self._label.set_hexpand(True) 478 self._label.set_ellipsize(Pango.EllipsizeMode.END) 479 self._label.set_xalign(0) 480 self._label.set_width_chars(18) 481 482 next_icon = Gtk.Image.new_from_icon_name('go-next-symbolic', 483 Gtk.IconSize.MENU) 484 next_icon.get_style_context().add_class('insensitive-fg-color') 485 486 account_enabled = app.settings.get_account_setting( 487 self._account, 'active') 488 self._switch = Gtk.Switch() 489 self._switch.set_active(account_enabled) 490 self._switch.set_vexpand(False) 491 492 self._switch_state_label = Gtk.Label() 493 self._switch_state_label.set_xalign(1) 494 self._switch_state_label.set_valign(Gtk.Align.CENTER) 495 self._set_label(account_enabled) 496 497 if (self._account == app.ZEROCONF_ACC_NAME and 498 not app.is_installed('ZEROCONF')): 499 self._switch.set_active(False) 500 self.set_activatable(False) 501 self.set_sensitive(False) 502 if sys.platform in ('win32', 'darwin'): 503 tooltip = _('Please check if Bonjour is installed.') 504 else: 505 tooltip = _('Please check if Avahi is installed.') 506 self.set_tooltip_text(tooltip) 507 508 self._switch.connect( 509 'state-set', self._on_enable_switch, self._account) 510 511 box.add(self._switch) 512 box.add(self._switch_state_label) 513 box.add(Gtk.Separator()) 514 box.add(self._label) 515 box.add(next_icon) 516 self.add(box) 517 518 @property 519 def account(self): 520 return self._account 521 522 @property 523 def label(self): 524 return self._label.get_text() 525 526 def update_account_label(self): 527 self._label.set_text(app.get_account_label(self._account)) 528 529 def enable_account(self, state): 530 self._switch.set_state(state) 531 self._set_label(state) 532 533 def _set_label(self, active): 534 text = Q_('?switch:On') if active else Q_('?switch:Off') 535 self._switch_state_label.set_text(text) 536 537 def _on_enable_switch(self, switch, state, account): 538 def _disable(): 539 app.connections[account].change_status('offline', 'offline') 540 app.interface.disable_account(account) 541 switch.set_state(state) 542 self._set_label(state) 543 544 old_state = app.settings.get_account_setting(account, 'active') 545 if old_state == state: 546 return Gdk.EVENT_PROPAGATE 547 548 if (account in app.connections and 549 not app.connections[account].state.is_disconnected): 550 # Connecting or connected 551 ConfirmationDialog( 552 _('Disable Account'), 553 _('Account %s is still connected') % account, 554 _('All chat and group chat windows will be closed.'), 555 [DialogButton.make('Cancel', 556 callback=lambda: switch.set_active(True)), 557 DialogButton.make('Remove', 558 text=_('_Disable Account'), 559 callback=_disable)], 560 transient_for=self.get_toplevel()).show() 561 return Gdk.EVENT_STOP 562 563 if state: 564 app.interface.enable_account(account) 565 else: 566 app.interface.disable_account(account) 567 568 return Gdk.EVENT_PROPAGATE 569 570 571class AddNewAccountPage(Gtk.Box): 572 def __init__(self): 573 Gtk.Box.__init__(self, 574 orientation=Gtk.Orientation.VERTICAL, 575 spacing=18) 576 self.set_vexpand(True) 577 self.set_hexpand(True) 578 self.set_margin_top(24) 579 pixbuf = Gtk.IconTheme.load_icon_for_scale( 580 Gtk.IconTheme.get_default(), 581 'org.gajim.Gajim-symbolic', 582 100, 583 self.get_scale_factor(), 584 0) 585 self.add(Gtk.Image.new_from_pixbuf(pixbuf)) 586 587 button = Gtk.Button(label=_('Add Account')) 588 button.get_style_context().add_class('suggested-action') 589 button.set_action_name('app.add-account') 590 button.set_halign(Gtk.Align.CENTER) 591 self.add(button) 592 593 594class GenericSettingPage(Gtk.Box): 595 def __init__(self, account, settings): 596 Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=12) 597 self.set_valign(Gtk.Align.START) 598 self.set_vexpand(True) 599 self.account = account 600 601 self.listbox = SettingsBox(account) 602 self.listbox.get_style_context().add_class('accounts-settings-border') 603 self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) 604 self.listbox.set_vexpand(False) 605 self.listbox.set_valign(Gtk.Align.END) 606 607 for setting in settings: 608 self.listbox.add_setting(setting) 609 self.listbox.update_states() 610 611 self.pack_end(self.listbox, True, True, 0) 612 613 def connect_signal(self, stack): 614 return stack.connect('notify::visible-child', 615 self._on_visible_child_changed) 616 617 def _on_visible_child_changed(self, stack, _param): 618 if self == stack.get_visible_child(): 619 self.listbox.update_states() 620 621 622class GeneralPage(GenericSettingPage): 623 624 name = 'general' 625 626 def __init__(self, account): 627 628 settings = [ 629 Setting(SettingKind.ENTRY, _('Label'), 630 SettingType.ACCOUNT_CONFIG, 'account_label', 631 callback=self._on_account_name_change), 632 633 Setting(SettingKind.COLOR, _('Color'), 634 SettingType.ACCOUNT_CONFIG, 'account_color', 635 desc=_('Recognize your account by color')), 636 637 Setting(SettingKind.LOGIN, _('Login'), SettingType.DIALOG, 638 bind='account::anonymous_auth', 639 inverted=True, 640 props={'dialog': LoginDialog}), 641 642 Setting(SettingKind.ACTION, _('Import Contacts'), 643 SettingType.ACTION, '-import-contacts', 644 props={'account': account}), 645 646 # Currently not supported by nbxmpp 647 # 648 # Setting(SettingKind.DIALOG, _('Client Certificate'), 649 # SettingType.DIALOG, props={'dialog': CertificateDialog}), 650 651 Setting(SettingKind.SWITCH, _('Connect on startup'), 652 SettingType.ACCOUNT_CONFIG, 'autoconnect'), 653 654 Setting(SettingKind.SWITCH, 655 _('Save conversations for all contacts'), 656 SettingType.ACCOUNT_CONFIG, 'no_log_for', 657 desc=_('Store conversations on the harddrive')), 658 659 Setting(SettingKind.SWITCH, _('Global Status'), 660 SettingType.ACCOUNT_CONFIG, 'sync_with_global_status', 661 desc=_('Synchronise the status of all accounts')), 662 663 Setting(SettingKind.SWITCH, _('Remember Last Status'), 664 SettingType.ACCOUNT_CONFIG, 'restore_last_status', 665 desc=_('Restore status and status message of your ' 666 'last session')), 667 668 Setting(SettingKind.SWITCH, _('Use file transfer proxies'), 669 SettingType.ACCOUNT_CONFIG, 'use_ft_proxies'), 670 ] 671 GenericSettingPage.__init__(self, account, settings) 672 673 def _on_account_name_change(self, *args): 674 self.get_toplevel().update_account_label(self.account) 675 676 677class PrivacyPage(GenericSettingPage): 678 679 name = 'privacy' 680 681 def __init__(self, account): 682 self._account = account 683 684 history_max_age = { 685 -1: _('Forever'), 686 86400: _('1 Day'), 687 604800: _('1 Week'), 688 2629743: _('1 Month'), 689 7889229: _('3 Months'), 690 15778458: _('6 Months'), 691 31556926: _('1 Year'), 692 } 693 694 chatstate_entries = { 695 'all': _('Enabled'), 696 'composing_only': _('Composing Only'), 697 'disabled': _('Disabled'), 698 } 699 700 settings = [ 701 Setting(SettingKind.SWITCH, _('Idle Time'), 702 SettingType.ACCOUNT_CONFIG, 'send_idle_time', 703 desc=_('Disclose the time of your last activity')), 704 705 Setting(SettingKind.SWITCH, _('Local System Time'), 706 SettingType.ACCOUNT_CONFIG, 'send_time_info', 707 desc=_('Disclose the local system time of the ' 708 'device Gajim runs on')), 709 710 Setting(SettingKind.SWITCH, _('Client / Operating System'), 711 SettingType.ACCOUNT_CONFIG, 'send_os_info', 712 desc=_('Disclose information about the client ' 713 'and operating system you currently use')), 714 715 Setting(SettingKind.SWITCH, _('Ignore Unknown Contacts'), 716 SettingType.ACCOUNT_CONFIG, 'ignore_unknown_contacts', 717 desc=_('Ignore everything from contacts not in your ' 718 'Roster')), 719 720 Setting(SettingKind.SWITCH, _('Send Message Receipts'), 721 SettingType.ACCOUNT_CONFIG, 'answer_receipts', 722 desc=_('Tell your contacts if you received a message')), 723 724 Setting(SettingKind.POPOVER, 725 _('Send Chatstate'), 726 SettingType.ACCOUNT_CONFIG, 727 'send_chatstate_default', 728 desc=_('Default for chats'), 729 props={'entries': chatstate_entries, 730 'button-text': _('Reset'), 731 'button-tooltip': _('Reset all chats to the ' 732 'current default value'), 733 'button-style': 'destructive-action', 734 'button-callback': self._reset_send_chatstate}), 735 736 Setting(SettingKind.POPOVER, 737 _('Send Chatstate in Group Chats'), 738 SettingType.ACCOUNT_CONFIG, 739 'gc_send_chatstate_default', 740 desc=_('Default for group chats'), 741 props={'entries': chatstate_entries, 742 'button-text': _('Reset'), 743 'button-tooltip': _('Reset all group chats to the ' 744 'current default value'), 745 'button-style': 'destructive-action', 746 'button-callback': self._reset_gc_send_chatstate}), 747 748 Setting(SettingKind.SWITCH, 749 _('Send Read Markers'), 750 SettingType.VALUE, 751 app.settings.get_account_setting( 752 account, 'send_marker_default'), 753 callback=self._send_read_marker, 754 desc=_('Default for chats and private group chats'), 755 props={'button-text': _('Reset'), 756 'button-tooltip': _('Reset all chats to the ' 757 'current default value'), 758 'button-style': 'destructive-action', 759 'button-callback': self._reset_send_read_marker}), 760 761 Setting(SettingKind.POPOVER, 762 _('Keep Chat History'), 763 SettingType.ACCOUNT_CONFIG, 764 'chat_history_max_age', 765 props={'entries': history_max_age}, 766 desc=_('How long Gajim should keep your chat history')), 767 ] 768 GenericSettingPage.__init__(self, account, settings) 769 770 @staticmethod 771 def _reset_send_chatstate(button): 772 button.set_sensitive(False) 773 app.settings.set_contact_settings('send_chatstate', None) 774 775 @staticmethod 776 def _reset_gc_send_chatstate(button): 777 button.set_sensitive(False) 778 app.settings.set_group_chat_settings('send_chatstate', None) 779 780 def _send_read_marker(self, state, _data): 781 app.settings.set_account_setting( 782 self._account, 'send_marker_default', state) 783 app.settings.set_account_setting( 784 self._account, 'gc_send_marker_private_default', state) 785 786 def _reset_send_read_marker(self, button): 787 button.set_sensitive(False) 788 app.settings.set_contact_settings('send_marker', None) 789 app.settings.set_group_chat_settings( 790 'send_marker', None, context='private') 791 for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account): 792 ctrl.update_actions() 793 794 795class ConnectionPage(GenericSettingPage): 796 797 name = 'connection' 798 799 def __init__(self, account): 800 801 settings = [ 802 Setting(SettingKind.POPOVER, _('Proxy'), 803 SettingType.ACCOUNT_CONFIG, 'proxy', name='proxy', 804 props={'entries': self._get_proxies(), 805 'default-text': _('System'), 806 'button-icon-name': 'preferences-system-symbolic', 807 'button-callback': self._on_proxy_edit}), 808 809 Setting(SettingKind.HOSTNAME, _('Hostname'), SettingType.DIALOG, 810 desc=_('Manually set the hostname for the server'), 811 props={'dialog': CutstomHostnameDialog}), 812 813 Setting(SettingKind.ENTRY, _('Resource'), 814 SettingType.ACCOUNT_CONFIG, 'resource'), 815 816 Setting(SettingKind.PRIORITY, _('Priority'), 817 SettingType.DIALOG, props={'dialog': PriorityDialog}), 818 819 Setting(SettingKind.SWITCH, _('Use Unencrypted Connection'), 820 SettingType.ACCOUNT_CONFIG, 'use_plain_connection', 821 desc=_('Use an unencrypted connection to the server')), 822 823 Setting(SettingKind.SWITCH, _('Confirm Unencrypted Connection'), 824 SettingType.ACCOUNT_CONFIG, 825 'confirm_unencrypted_connection', 826 desc=_('Show a confirmation dialog before connecting ' 827 'unencrypted')), 828 ] 829 GenericSettingPage.__init__(self, account, settings) 830 831 @staticmethod 832 def _get_proxies(): 833 return {proxy: proxy for proxy in app.settings.get_proxies()} 834 835 @staticmethod 836 def _on_proxy_edit(*args): 837 open_window('ManageProxies') 838 839 def update_proxy_entries(self): 840 self.listbox.get_setting('proxy').update_entries(self._get_proxies()) 841 842 843class AdvancedPage(GenericSettingPage): 844 845 name = 'advanced' 846 847 def __init__(self, account): 848 849 settings = [ 850 Setting(SettingKind.SWITCH, _('Contact Information'), 851 SettingType.ACCOUNT_CONFIG, 'request_user_data', 852 desc=_('Request contact information ' 853 '(Mood, Activity, Tune, Location)')), 854 855 Setting(SettingKind.SWITCH, _('Accept all Contact Requests'), 856 SettingType.ACCOUNT_CONFIG, 'autoauth', 857 desc=_('Automatically accept all contact requests')), 858 859 Setting(SettingKind.POPOVER, _('Filetransfer Preference'), 860 SettingType.ACCOUNT_CONFIG, 'filetransfer_preference', 861 props={'entries': {'httpupload': _('Upload Files'), 862 'jingle': _('Send Files Directly')}}, 863 desc=_('Preferred file transfer mechanism for ' 864 'file drag&drop on a chat window')) 865 ] 866 GenericSettingPage.__init__(self, account, settings) 867 868 869class ZeroConfPage(GenericSettingPage): 870 871 name = 'general' 872 873 def __init__(self, account): 874 875 settings = [ 876 Setting(SettingKind.DIALOG, _('Profile'), 877 SettingType.DIALOG, 878 props={'dialog': ZeroconfProfileDialog}), 879 880 Setting(SettingKind.SWITCH, _('Connect on startup'), 881 SettingType.ACCOUNT_CONFIG, 'autoconnect', 882 desc=_('Use environment variable')), 883 884 Setting(SettingKind.SWITCH, 885 _('Save conversations for all contacts'), 886 SettingType.ACCOUNT_CONFIG, 'no_log_for', 887 desc=_('Store conversations on the harddrive')), 888 889 Setting(SettingKind.SWITCH, _('Global Status'), 890 SettingType.ACCOUNT_CONFIG, 'sync_with_global_status', 891 desc=_('Synchronize the status of all accounts')), 892 ] 893 894 GenericSettingPage.__init__(self, account, settings) 895 896 897class ZeroconfProfileDialog(SettingsDialog): 898 def __init__(self, account, parent): 899 900 settings = [ 901 Setting(SettingKind.ENTRY, _('First Name'), 902 SettingType.ACCOUNT_CONFIG, 'zeroconf_first_name'), 903 904 Setting(SettingKind.ENTRY, _('Last Name'), 905 SettingType.ACCOUNT_CONFIG, 'zeroconf_last_name'), 906 907 Setting(SettingKind.ENTRY, _('XMPP Address'), 908 SettingType.ACCOUNT_CONFIG, 'zeroconf_jabber_id'), 909 910 Setting(SettingKind.ENTRY, _('Email'), 911 SettingType.ACCOUNT_CONFIG, 'zeroconf_email'), 912 ] 913 914 SettingsDialog.__init__(self, parent, _('Profile'), 915 Gtk.DialogFlags.MODAL, settings, account) 916 917 918class PriorityDialog(SettingsDialog): 919 def __init__(self, account, parent): 920 921 neg_priority = app.settings.get('enable_negative_priority') 922 if neg_priority: 923 range_ = (-128, 127) 924 else: 925 range_ = (0, 127) 926 927 settings = [ 928 Setting(SettingKind.SWITCH, _('Adjust to status'), 929 SettingType.ACCOUNT_CONFIG, 930 'adjust_priority_with_status'), 931 932 Setting(SettingKind.SPIN, _('Priority'), 933 SettingType.ACCOUNT_CONFIG, 934 'priority', 935 bind='account::adjust_priority_with_status', 936 inverted=True, 937 props={'range_': range_}), 938 ] 939 940 SettingsDialog.__init__(self, parent, _('Priority'), 941 Gtk.DialogFlags.MODAL, settings, account) 942 943 self.connect('destroy', self.on_destroy) 944 945 def on_destroy(self, *args): 946 # Update priority 947 if self.account not in app.connections: 948 return 949 show = app.connections[self.account].status 950 status = app.connections[self.account].status_message 951 app.connections[self.account].change_status(show, status) 952 953 954class CutstomHostnameDialog(SettingsDialog): 955 def __init__(self, account, parent): 956 957 type_values = ('START TLS', 'DIRECT TLS', 'PLAIN') 958 959 settings = [ 960 Setting(SettingKind.SWITCH, _('Enable'), 961 SettingType.ACCOUNT_CONFIG, 962 'use_custom_host'), 963 964 Setting(SettingKind.ENTRY, _('Hostname'), 965 SettingType.ACCOUNT_CONFIG, 'custom_host', 966 bind='account::use_custom_host'), 967 968 Setting(SettingKind.SPIN, _('Port'), 969 SettingType.ACCOUNT_CONFIG, 'custom_port', 970 bind='account::use_custom_host', 971 props={'range_': (0, 65535)},), 972 973 Setting(SettingKind.COMBO, _('Type'), 974 SettingType.ACCOUNT_CONFIG, 'custom_type', 975 bind='account::use_custom_host', 976 props={'combo_items': type_values}), 977 ] 978 979 SettingsDialog.__init__(self, parent, _('Connection Settings'), 980 Gtk.DialogFlags.MODAL, settings, account) 981 982 983class CertificateDialog(SettingsDialog): 984 def __init__(self, account, parent): 985 986 settings = [ 987 Setting(SettingKind.FILECHOOSER, _('Client Certificate'), 988 SettingType.ACCOUNT_CONFIG, 'client_cert', 989 props={'filefilter': (_('PKCS12 Files'), '*.p12')}), 990 991 Setting(SettingKind.SWITCH, _('Encrypted Certificate'), 992 SettingType.ACCOUNT_CONFIG, 'client_cert_encrypted'), 993 ] 994 995 SettingsDialog.__init__(self, parent, _('Certificate Settings'), 996 Gtk.DialogFlags.MODAL, settings, account) 997 998 999class LoginDialog(SettingsDialog): 1000 def __init__(self, account, parent): 1001 1002 settings = [ 1003 Setting(SettingKind.ENTRY, _('Password'), 1004 SettingType.ACCOUNT_CONFIG, 'password', 1005 bind='account::savepass'), 1006 1007 Setting(SettingKind.SWITCH, _('Save Password'), 1008 SettingType.ACCOUNT_CONFIG, 'savepass'), 1009 1010 Setting(SettingKind.CHANGEPASSWORD, _('Change Password'), 1011 SettingType.DIALOG, callback=self.on_password_change, 1012 props={'dialog': None}), 1013 1014 Setting(SettingKind.SWITCH, _('Use GSSAPI'), 1015 SettingType.ACCOUNT_CONFIG, 'enable_gssapi'), 1016 ] 1017 1018 SettingsDialog.__init__(self, parent, _('Login Settings'), 1019 Gtk.DialogFlags.MODAL, settings, account) 1020 1021 self.connect('destroy', self.on_destroy) 1022 1023 def on_password_change(self, new_password, _data): 1024 passwords.save_password(self.account, new_password) 1025 1026 def on_destroy(self, *args): 1027 savepass = app.settings.get_account_setting(self.account, 'savepass') 1028 if not savepass: 1029 passwords.delete_password(self.account) 1030