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