diff --git a/data/gui/accounts_window.ui b/data/gui/accounts_window.ui index de0e541c6..60416ad0c 100644 --- a/data/gui/accounts_window.ui +++ b/data/gui/accounts_window.ui @@ -1,1573 +1,114 @@ - + - - - 127 - 5 - 1 - 5 - - + + True False - gtk-add - - - True - False - gtk-remove - - - - - - - - - None - - - - - True - False - gtk-missing-image - - - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 12 - Accounts - dialog - - + vertical - + True False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 6 + - - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 175 - True - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - vertical - 6 - - - 170 - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - in - - - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False - - - - - - - - - True - True - 0 - - - - - Add - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - image1 - True - - - - False - True - 1 - - - - - Delete - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - image2 - True - - - - False - True - 2 - - - - - Re_name - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - rename_image - True - - - - False - True - 3 - - - - - False - False - - - - - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - False - False - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - - - - - - True - False - vertical - - - Active - True - True - False - True - 0.5 - True - - - - False - False - 0 - - - - - True - True - - - True - False - 6 - 6 - 6 - - - True - False - _Jabber ID: - True - 0 - - - 0 - 0 - - - - - True - False - _Password: - True - 0 - - - 0 - 1 - - - - - True - False - Resour_ce: - True - 0 - - - 0 - 2 - - - - - True - False - Priori_ty: - True - 0 - - - 0 - 3 - - - - - True - True - True - - - - 1 - 0 - - - - - True - False - True - False - True - - - - 1 - 1 - - - - - True - True - Resource is sent to the Jabber server in order to separate the same JID in two or more parts depending on the number of the clients connected in the same server with the same account. So you might be connected in the same account with resource 'Home' and 'Work' at the same time. The resource which has the highest priority will get the events. (see below) - Gajim - - - - 1 - 2 - 2 - - - - - A_djust to status - True - True - False - Priority will change automatically according to your status. - True - 0.5 - True - - - - 1 - 3 - - - - - Anonymous authentication - True - True - False - 0.5 - True - - - - 2 - 0 - - - - - Save pass_word - True - True - False - If checked, Gajim will remember the password for this account - True - False - 0.5 - True - - - - 2 - 1 - - - - - True - True - Priority is used in Jabber to determine who gets the events from the jabber server when two or more clients are connected using the same account; The client with the highest priority gets the events - adjustment1 - 1 - True - - - - 2 - 3 - - - - - True - True - - - True - False - 11 - True - - - Synchronize contacts - True - True - False - Click to request authorization to all contacts of another account - True - - - - False - True - 0 - - - - - Chan_ge Password - True - True - False - Click to change account's password - True - - - - False - True - 1 - - - - - - - True - False - Administration operations - - - - - 0 - 4 - 3 - - - - - True - True - - - True - False - vertical - 6 - - - True - False - 6 - - - True - False - _Client Cert File: - True - cert_entry1 - 0 - - - False - True - 0 - - - - - True - True - - - - - True - True - 1 - - - - - Browse... - True - True - True - True - - - - False - False - 2 - - - - - True - True - 0 - - - - - Certificate is e_ncrypted - True - True - False - True - 0.5 - True - - - - True - True - 1 - - - - - - - True - False - Client certificate - - - - - 0 - 5 - 3 - - - - - - - True - False - Account - - - False - - - - - True - False - 6 - vertical - 6 - - - C_onnect on Gajim startup - True - True - False - If checked, Gajim, when launched, will automatically connect to jabber using this account - True - 0.5 - True - - - - False - False - 0 - - - - - Auto-reconnect when connection is lost - True - True - False - True - 0.5 - True - - - - False - False - 1 - - - - - Save conversation _logs for all contacts - True - True - False - True - 0.5 - True - True - - - - False - False - 2 - - - - - Synchronize logs with server - True - True - False - True - 0.5 - True - True - - - - False - False - 3 - - - - - Synch_ronize account status with global status - True - True - False - If checked, any change to the global status (handled by the combobox at the bottom of the roster window) will change the status of this account accordingly - True - 0.5 - True - - - - False - False - 4 - - - - - Receive conversations from other resources (provided the server has support for it) - True - True - False - True - 0.5 - True - - - - False - False - 5 - - - - - Use file transfer proxies - True - True - False - If checked, Gajim will also broadcast some more IPs except from just your IP, so file transfer has higher chances of working. - True - 0.5 - True - - - - False - False - 6 - - - - - 1 - - - - - True - False - General - True - - - 1 - False - - - - - True - False - 6 - vertical - 12 - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - none - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 12 - 6 - vertical - 6 - - - _use HTTP__PROXY environment variable - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - 0 - True - - - - False - True - 0 - - - - - True - False - 6 - - - True - False - liststore1 - - - - - 0 - - - - - True - True - 0 - - - - - _Manage... - True - True - False - True - - - - False - False - 1 - - - - - False - True - 1 - - - - - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - <b>Proxy</b> - True - - - - - False - True - 0 - - - - - True - False - 0 - none - - - True - False - 12 - 6 - vertical - 6 - - - _Warn before using an insecure connection - True - True - False - Check this so Gajim will ask you before sending your password over an insecure connection. - True - 0 - True - - - - False - False - 0 - - - - - Send _keep-alive packets - True - True - False - If checked, Gajim will send keep-alive packets to prevent connection timeout which results in disconnection - True - 0 - True - True - - - - False - False - 1 - - - - - Use cust_om hostname/port - True - True - False - True - 0 - True - - - - False - False - 2 - - - - - True - False - False - 6 - - - True - False - _Hostname: - True - - - False - False - 0 - - - - - True - True - - - - True - True - 1 - - - - - True - False - _Port: - True - - - False - False - 2 - - - - - True - True - 6 - 5222 - - - - False - True - 3 - - - - - True - True - 3 - - - - - - - True - False - <b>Miscellaneous</b> - True - - - - - False - True - 1 - - - - - 2 - - - - - True - False - Connection - - - 2 - False - - - - - True - False - 5 - vertical - 6 - - - True - False - 0 - none - - - True - False - 12 - 6 - vertical - 6 - - - True - False - 6 - - - True - False - No key selected - True - - - False - False - 0 - - - - - True - False - True - end - - - True - True - 1 - - - - - Choose _Key... - True - True - False - True - - - - False - False - 2 - - - - - False - True - 0 - - - - - Use G_PG Agent - True - False - True - False - If checked, Gajim will get the password from a GPG agent like seahorse - True - 0 - True - - - - False - False - 1 - - - - - - - True - False - <b>OpenPGP</b> - True - - - - - False - True - 0 - - - - - True - False - 0 - none - - - _Edit Personal Information... - True - True - False - Information about you, as stored in the server - 12 - 6 - True - - - - - - True - False - <b>Personal Information</b> - True - - - - - False - True - 1 - - - - - 3 - - - - - True - False - Personal Information - - - 3 - False - - - - - True - True - 1 - - - - - 1 - - - - - - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - - - Active - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - 0.5 - True - - - - False - True - 0 - - - - - True - True - - - True - False - 6 - vertical - 6 - - - Co_nnect on Gajim startup - True - True - False - If checked, Gajim, when launched, will automatically connect to jabber using this account - True - 0.5 - True - - - - False - False - 0 - - - - - Save conversation _logs for all contacts - True - True - False - True - 0.5 - True - - - - False - False - 1 - - - - - Synchroni_ze account status with global status - True - True - False - If checked, any change to the global status (handled by the combobox at the bottom of the roster window) will change the status of this account accordingly - True - 0.5 - True - - - - False - False - 2 - - - - - True - False - - - Use cust_om port: - True - True - False - If the default port that is used for incoming messages is unfitting for your setup you can select another one here. -You might consider to change possible firewall settings. - True - 0.5 - True - - - - False - False - 0 - - - - - True - True - 6 - - - - False - False - 1 - - - - - False - True - 10 - 3 - - - - - - - True - False - General - True - - - False - - - - - True - False - 11 - 5 - 5 - - - True - False - vertical - 5 - - - True - False - <b>OpenPGP</b> - True - 0 - - - False - False - 0 - - - - - True - False - 6 - - - True - False - No key selected - - - False - False - 0 - - - - - True - False - True - end - - - True - True - 1 - - - - - Choose _Key... - True - True - False - True - - - - False - False - 2 - - - - - False - True - 1 - - - - - Use G_PG Agent - True - True - False - If checked, Gajim will get the password from a GPG agent like seahorse - True - 0.5 - True - - - - False - False - 2 - - - - - 0 - 0 - 2 - - - - - True - False - <b>Personal Information</b> - True - 0 - - - 0 - 1 - 2 - - - - - True - False - First Name: - 1 - - - 0 - 2 - - - - - True - False - Last Name: - 1 - - - 0 - 3 - - - - - True - False - Jabber ID: - 1 - - - 0 - 4 - - - - - True - False - E-Mail: - 1 - - - 0 - 5 - - - - - True - True - True - - - - 1 - 2 - - - - - True - True - True - - - - 1 - 3 - - - - - True - True - True - - - - 1 - 4 - - - - - True - True - True - - - - 1 - 5 - - - - - 1 - - - - - True - False - Personal Information - - - 1 - False - - - - - True - True - 1 - - - - - 2 - - - - - - - - True - False - - - - - True - True - 0 - - - - - Mer_ge accounts - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - 0.5 - True - - - False - True - 1 - - - - + True False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 6 - end + vertical - - gtk-close + True True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - + never + in + 400 + True + + + True + False + + + OptionsBox + True + False + none + + + + + False - False + True 0 + + + True + False + False + + + False + True + 1 + + - False - True - 3 + main + page0 + + False + True + 0 + + + + + True + False + Accounts + True + :close + + + True + True + True + + + + True + False + go-previous-symbolic + + + + + + + True + True + True + + + True + False + open-menu-symbolic + + + + + 1 + diff --git a/data/gui/synchronise_select_account_dialog.ui b/data/gui/synchronise_select_account_dialog.ui index 58d066d46..365f862c1 100644 --- a/data/gui/synchronise_select_account_dialog.ui +++ b/data/gui/synchronise_select_account_dialog.ui @@ -9,6 +9,7 @@ 350 300 dialog + True diff --git a/data/style/gajim.css b/data/style/gajim.css index f735bc487..62c782586 100644 --- a/data/style/gajim.css +++ b/data/style/gajim.css @@ -30,14 +30,34 @@ popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; } #ServerInfoGrid > list > label { padding:10px; color: @insensitive_fg_color; font-weight: bold; } #ServerInfoGrid > list > row.activatable:active { box-shadow: none; } -/* Generic Options Dialog */ -#OptionsDialog list > row { border-bottom: 1px solid; border-color: @theme_unfocused_bg_color; } -#OptionsDialog list > row:last-child { border-bottom: 0px} -#OptionsDialog list > row { padding: 10px; } -#OptionsDialog list > row.activatable:active { box-shadow: none; } +/* OptionsBox */ +#OptionsBox > row { border-bottom: 1px solid; border-color: @theme_unfocused_bg_color; } +#OptionsBox > row:last-child { border-bottom: 0px} +#OptionsBox > row.activatable:active { box-shadow: none; } +#OptionsBox > row { padding: 10px 20px 10px 10px; } +#OptionsBox > row:not(.activatable) label { color: @insensitive_fg_color } + +/* GenericOption */ +#SubDescription { color: @insensitive_fg_color;} +#GenericOptionBox { margin-left: 30px; } +#GenericOptionBox > label { padding-right: 3px; } /* Generic Popover Menu with Buttons */ .PopoverButtonListbox { padding-left: 0px; padding-right: 0px; } .PopoverButtonListbox > list { margin-top: 10px; margin-bottom: 10px; } .PopoverButtonListbox > list > row { padding: 10px 20px 10px 20px; } .PopoverButtonListbox > list > row.activatable:active { box-shadow: none; background-color: @theme_selected_bg_color } + +/* Accounts Window */ +#AccountsWindow > box { padding:30px 30px 30px 30px;} +#AccountsWindow scrolledwindow {border: none;} +#AccountsWindow list {border: 1px solid; border-color: @borders;} +#AccountsWindow > box actionbar box {border: none;} + +#AccountNameEntry:disabled { font-size: 16px; + font-weight: bold; + border: none; + background-color: @theme_unfocused_bg_color; + color: @theme_text_color; } + + diff --git a/gajim/accounts_window.py b/gajim/accounts_window.py new file mode 100644 index 000000000..3b4f3df1a --- /dev/null +++ b/gajim/accounts_window.py @@ -0,0 +1,698 @@ +from functools import partial + +from gi.repository import Gtk, Gio, GLib, Gdk + +from gajim.common import app +from gajim.gtkgui_helpers import get_image_button +from gajim import gtkgui_helpers +from gajim import gui_menu_builder +from gajim.common import passwords +from gajim import dialogs +from gajim import config +from gajim.common import helpers +from gajim.common.connection import Connection +from gajim.common.zeroconf.connection_zeroconf import ConnectionZeroconf +from gajim.options_dialog import OptionsDialog, OptionsBox +from gajim.common.const import Option, OptionKind, OptionType + + +class AccountsWindow(Gtk.ApplicationWindow): + def __init__(self): + Gtk.ApplicationWindow.__init__(self) + self.set_application(app.app) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_show_menubar(False) + self.set_name('AccountsWindow') + self.set_size_request(500, -1) + self.set_resizable(False) + self.need_relogin = {} + + glade_objects = [ + 'stack', 'box', 'actionbar', 'headerbar', 'back_button', + 'menu_button', 'account_page', 'account_list'] + self.builder = gtkgui_helpers.get_gtk_builder('accounts_window.ui') + for obj in glade_objects: + setattr(self, obj, self.builder.get_object(obj)) + + self.set_titlebar(self.headerbar) + + menu = Gio.Menu() + menu.append('Merge Accounts', 'app.merge') + menu.append('Use PGP Agent', 'app.agent') + self.menu_button.set_menu_model(menu) + + button = get_image_button('list-add-symbolic', 'Add') + button.set_action_name('app.add-account') + self.actionbar.pack_start(button) + + accounts = app.config.get_per('accounts') + accounts.sort() + for account in accounts: + self.need_relogin[account] = self.get_relogin_options(account) + account_item = Account(account, self) + self.account_list.add(account_item) + account_item.set_activatable() + + self.add(self.box) + self.builder.connect_signals(self) + + self.connect('destroy', self.on_destroy) + self.connect('key-press-event', self.on_key_press) + self.show_all() + + def on_key_press(self, widget, event): + if event.keyval == Gdk.KEY_Escape: + self.destroy() + + def on_destroy(self, *args): + self.check_relogin() + del app.interface.instances['accounts'] + + def on_child_visible(self, stack, *args): + page = stack.get_visible_child_name() + if page is None: + return + if page == 'main': + self.menu_button.show() + self.back_button.hide() + self.check_relogin() + else: + self.back_button.show() + self.menu_button.hide() + + def on_back_button(self, *args): + page = self.stack.get_visible_child_name() + child = self.stack.get_visible_child() + self.remove_all_pages() + if page == 'account': + child.toggle.set_active(False) + self.stack.add_named(self.account_page, 'main') + self.stack.set_visible_child_name('main') + self.update_accounts() + else: + self.stack.add_named(child.parent, 'account') + self.stack.set_visible_child_name('account') + + def update_accounts(self): + for row in self.account_list.get_children(): + row.get_child().update() + + @staticmethod + def on_row_activated(listbox, row): + row.get_child().on_row_activated() + + def remove_all_pages(self): + for page in self.stack.get_children(): + self.stack.remove(page) + + def set_page(self, page, name): + self.remove_all_pages() + self.stack.add_named(page, name) + page.update() + page.show_all() + self.stack.set_visible_child(page) + + def update_proxy_list(self): + page = self.stack.get_child_by_name('connetion') + if page is None: + return + page.options['proxy'].update_values() + + def check_relogin(self): + for account in self.need_relogin: + options = self.get_relogin_options(account) + active = app.config.get_per('accounts', account, 'active') + if options != self.need_relogin[account]: + self.need_relogin[account] = options + if active: + self.relog(account) + break + + def relog(self, account): + if app.connections[account].connected == 0: + return + + if account == app.ZEROCONF_ACC_NAME: + app.connections[app.ZEROCONF_ACC_NAME].update_details() + return + + def login(account, show_before, status_before): + """ + Login with previous status + """ + # first make sure connection is really closed, + # 0.5 may not be enough + app.connections[account].disconnect(True) + app.interface.roster.send_status( + account, show_before, status_before) + + def relog(account): + show_before = app.SHOW_LIST[app.connections[account].connected] + status_before = app.connections[account].status + app.interface.roster.send_status( + account, 'offline', _('Be right back.')) + GLib.timeout_add(500, login, account, show_before, status_before) + + dialogs.YesNoDialog( + _('Relogin now?'), + _('If you want all the changes to apply instantly, ' + 'you must relogin.'), + transient_for=self, + on_response_yes=lambda *args: relog(account)) + + @staticmethod + def get_relogin_options(account): + if account == app.ZEROCONF_ACC_NAME: + options = ['zeroconf_first_name', 'zeroconf_last_name', + 'zeroconf_jabber_id', 'zeroconf_email', 'keyid'] + else: + options = ['client_cert', 'proxy', 'resource', + 'use_custom_host', 'custom_host', 'custom_port', + 'keyid'] + + values = [] + for option in options: + values.append(app.config.get_per('accounts', account, option)) + return values + + def on_remove_account(self, button, account): + if app.events.get_events(account): + dialogs.ErrorDialog( + _('Unread events'), + _('Read all pending events before removing this account.'), + transient_for=self) + return + + if app.config.get_per('accounts', account, 'is_zeroconf'): + # Should never happen as button is insensitive + return + + win_opened = False + if app.interface.msg_win_mgr.get_controls(acct=account): + win_opened = True + elif account in app.interface.instances: + for key in app.interface.instances[account]: + if (app.interface.instances[account][key] and + key != 'remove_account'): + win_opened = True + break + + # Detect if we have opened windows for this account + + def remove(account): + if (account in app.interface.instances and + 'remove_account' in app.interface.instances[account]): + dialog = app.interface.instances[account]['remove_account'] + dialog.window.present() + else: + if account not in app.interface.instances: + app.interface.instances[account] = {} + app.interface.instances[account]['remove_account'] = \ + config.RemoveAccountWindow(account) + if win_opened: + dialogs.ConfirmationDialog( + _('You have opened chat in account %s') % account, + _('All chat and groupchat windows will be closed. ' + 'Do you want to continue?'), + on_response_ok=(remove, account)) + else: + remove(account) + + def remove_account(self, account): + for row in self.account_list.get_children(): + if row.get_child().account == account: + self.account_list.remove(row) + del self.need_relogin[account] + break + + def add_account(self, account): + account_item = Account(account, self) + self.account_list.add(account_item) + account_item.set_activatable() + self.account_list.show_all() + self.stack.show_all() + self.need_relogin[account] = self.get_relogin_options(account) + + def select_account(self, account): + for row in self.account_list.get_children(): + if row.get_child().account == account: + self.account_list.emit('row-activated', row) + break + + @staticmethod + def enable_account(account): + if account == app.ZEROCONF_ACC_NAME: + app.connections[account] = ConnectionZeroconf(account) + else: + app.connections[account] = Connection(account) + + # update variables + app.interface.instances[account] = { + 'infos': {}, 'disco': {}, 'gc_config': {}, 'search': {}, + 'online_dialog': {}, 'sub_request': {}} + app.interface.minimized_controls[account] = {} + app.connections[account].connected = 0 + app.groups[account] = {} + app.contacts.add_account(account) + app.gc_connected[account] = {} + app.automatic_rooms[account] = {} + app.newly_added[account] = [] + app.to_be_removed[account] = [] + if account == app.ZEROCONF_ACC_NAME: + app.nicks[account] = app.ZEROCONF_ACC_NAME + else: + app.nicks[account] = app.config.get_per( + 'accounts', account, 'name') + app.block_signed_in_notifications[account] = True + app.sleeper_state[account] = 'off' + app.encrypted_chats[account] = [] + app.last_message_time[account] = {} + app.status_before_autoaway[account] = '' + app.transport_avatar[account] = {} + app.gajim_optional_features[account] = [] + app.caps_hash[account] = '' + helpers.update_optional_features(account) + # refresh roster + if len(app.connections) >= 2: + # Do not merge accounts if only one exists + app.interface.roster.regroup = app.config.get('mergeaccounts') + else: + app.interface.roster.regroup = False + app.interface.roster.setup_and_draw_roster() + gui_menu_builder.build_accounts_menu() + + @staticmethod + def disable_account(account): + app.interface.roster.close_all(account) + if account == app.ZEROCONF_ACC_NAME: + app.connections[account].disable_account() + app.connections[account].cleanup() + del app.connections[account] + del app.interface.instances[account] + del app.interface.minimized_controls[account] + del app.nicks[account] + del app.block_signed_in_notifications[account] + del app.groups[account] + app.contacts.remove_account(account) + del app.gc_connected[account] + del app.automatic_rooms[account] + del app.to_be_removed[account] + del app.newly_added[account] + del app.sleeper_state[account] + del app.encrypted_chats[account] + del app.last_message_time[account] + del app.status_before_autoaway[account] + del app.transport_avatar[account] + del app.gajim_optional_features[account] + del app.caps_hash[account] + if len(app.connections) >= 2: + # Do not merge accounts if only one exists + app.interface.roster.regroup = app.config.get('mergeaccounts') + else: + app.interface.roster.regroup = False + app.interface.roster.setup_and_draw_roster() + gui_menu_builder.build_accounts_menu() + + +class Account(Gtk.Box): + def __init__(self, account, parent): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, + spacing=12) + self.account = account + if account == app.ZEROCONF_ACC_NAME: + self.options = ZeroConfPage(account) + else: + self.options = AccountPage(account) + self.parent = parent + + switch = Gtk.Switch() + switch.set_active(app.config.get_per('accounts', account, 'active')) + switch.set_vexpand(False) + switch.set_valign(Gtk.Align.CENTER) + switch.set_halign(Gtk.Align.START) + if account == app.ZEROCONF_ACC_NAME and not app.HAVE_ZEROCONF: + switch.set_sensitive(False) + switch.set_active(False) + switch.connect('notify::active', self.on_switch, self.account) + + account_label = app.config.get_per('accounts', account, 'account_label') + self.label = Gtk.Label(label=account_label or account) + self.label.set_halign(Gtk.Align.START) + self.label.set_hexpand(True) + + self.add(switch) + self.add(self.label) + + if account != app.ZEROCONF_ACC_NAME: + button = get_image_button('list-remove-symbolic', 'Remove') + button.connect('clicked', parent.on_remove_account, account) + self.add(button) + + def set_activatable(self): + if self.account == app.ZEROCONF_ACC_NAME: + self.get_parent().set_activatable(app.HAVE_ZEROCONF) + + def on_switch(self, switch, param, account): + old_state = app.config.get_per('accounts', account, 'active') + state = switch.get_active() + if old_state == state: + return + + if (account in app.connections and + app.connections[account].connected > 0): + # connecting or connected + dialogs.ErrorDialog( + _('You are currently connected to the server'), + _('To disable the account, you must be disconnected.'), + transient_for=self.parent) + switch.set_active(not state) + return + if state: + self.parent.enable_account(account) + else: + self.parent.disable_account(account) + app.config.set_per('accounts', account, 'active', state) + + def on_row_activated(self): + self.options.update_states() + self.parent.set_page(self.options, 'account') + + def update(self): + account_label = app.config.get_per( + 'accounts', self.account, 'account_label') + self.label.set_text(account_label or self.account) + + +class GenericOptionPage(Gtk.Box): + def __init__(self, account, parent, options): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=12) + self.account = account + self.parent = parent + + self.toggle = get_image_button('document-edit-symbolic', + _('Rename account label'), toggle=True) + self.toggle.connect('toggled', self.set_entry_text) + + self.entry = Gtk.Entry() + self.entry.set_sensitive(False) + self.entry.set_name('AccountNameEntry') + self.set_entry_text(self.toggle, update=True) + + box = Gtk.Box() + if isinstance(self, AccountPage): + box.pack_start(self.toggle, False, True, 0) + box.pack_start(self.entry, True, True, 0) + + self.listbox = OptionsBox(account) + self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) + + for option in options: + self.listbox.add_option(option) + self.listbox.update_states() + + self.pack_start(box, False, False, 0) + self.pack_start(self.listbox, True, True, 0) + + self.listbox.connect('row-activated', self.on_row_activated) + + def update_states(self): + self.listbox.update_states() + + def on_row_activated(self, listbox, row): + self.toggle.set_active(False) + row.get_child().on_row_activated() + + def set_entry_text(self, toggle, update=False): + account_label = app.config.get_per( + 'accounts', self.account, 'account_label') + if update: + self.entry.set_text(account_label or self.account) + return + if toggle.get_active(): + self.entry.set_sensitive(True) + self.entry.grab_focus() + else: + self.entry.set_sensitive(False) + value = self.entry.get_text() + if not value: + value = account_label or self.account + app.config.set_per('accounts', self.account, + 'account_label', value or self.account) + if app.config.get_per('accounts', self.account, 'active'): + app.interface.roster.draw_account(self.account) + + def update(self): + self.set_entry_text(self.toggle, update=True) + + def set_page(self, options, name): + options.update_states() + self.get_toplevel().set_page(options, name) + + +class AccountPage(GenericOptionPage): + def __init__(self, account, parent=None): + + general = partial( + self.set_page, GeneralPage(account, self), 'general') + connection = partial( + self.set_page, ConnectionPage(account, self), 'connection') + + options = [ + Option(OptionKind.LOGIN, _('Login'), OptionType.DIALOG, + props={'dialog': LoginDialog}), + + Option(OptionKind.ACTION, _('Profile'), OptionType.ACTION, + '-profile', props={'action_args': account}), + + Option(OptionKind.CALLBACK, _('General'), + name='general', props={'callback': general}), + + Option(OptionKind.CALLBACK, _('Connection'), + name='connection', props={'callback': connection}), + + Option(OptionKind.ACTION, _('Import Contacts'), OptionType.ACTION, + '-import-contacts', props={'action_args': account}), + + Option(OptionKind.DIALOG, _('Client Certificate'), + OptionType.DIALOG, props={'dialog': CertificateDialog}), + + Option(OptionKind.GPG, _('OpenPGP Key'), OptionType.DIALOG, + props={'dialog': None}), + ] + + GenericOptionPage.__init__(self, account, parent, options) + + +class GeneralPage(GenericOptionPage): + def __init__(self, account, parent=None): + + options = [ + Option(OptionKind.SWITCH, _('Connect on startup'), + OptionType.ACCOUNT_CONFIG, 'autoconnect'), + + Option(OptionKind.SWITCH, _('Reconnect when connection is lost'), + OptionType.ACCOUNT_CONFIG, 'autoreconnect'), + + Option(OptionKind.SWITCH, _('Save conversations for all contacts'), + OptionType.ACCOUNT_CONFIG, 'no_log_for', + desc=_('Store conversations on the harddrive')), + + Option(OptionKind.SWITCH, _('Server Message Archive'), + OptionType.ACCOUNT_CONFIG, 'sync_logs_with_server', + desc=_('Messages get stored on the server.\n' + 'The archive is used to sync messages\n' + 'between multiple devices.\n' + 'XEP-0313')), + + Option(OptionKind.SWITCH, _('Global Status'), + OptionType.ACCOUNT_CONFIG, 'sync_with_global_status', + desc=_('Synchronise the status of all accounts')), + + Option(OptionKind.SWITCH, _('Message Carbons'), + OptionType.ACCOUNT_CONFIG, 'enable_message_carbons', + desc=_('All your other online devices get copies\n' + 'of sent and received messages.\n' + 'XEP-0280')), + + Option(OptionKind.SWITCH, _('Use file transfer proxies'), + OptionType.ACCOUNT_CONFIG, 'use_ft_proxies'), + ] + GenericOptionPage.__init__(self, account, parent, options) + + +class ConnectionPage(GenericOptionPage): + def __init__(self, account, parent=None): + + options = [ + Option(OptionKind.SWITCH, 'HTTP_PROXY', + OptionType.ACCOUNT_CONFIG, 'use_env_http_proxy', + desc=_('Use environment variable')), + + Option(OptionKind.PROXY, _('Proxy'), + OptionType.ACCOUNT_CONFIG, 'proxy', name='proxy'), + + Option(OptionKind.SWITCH, _('Warn on insecure connection'), + OptionType.ACCOUNT_CONFIG, + 'warn_when_insecure_ssl_connection'), + + Option(OptionKind.SWITCH, _('Send keep-alive packets'), + OptionType.ACCOUNT_CONFIG, 'keep_alives_enabled'), + + Option(OptionKind.HOSTNAME, _('Hostname'), OptionType.DIALOG, + desc=_('Manually set the hostname for the server'), + props={'dialog': CutstomHostnameDialog}), + + Option(OptionKind.ENTRY, _('Resource'), + OptionType.ACCOUNT_CONFIG, 'resource'), + + Option(OptionKind.PRIORITY, _('Priority'), + OptionType.DIALOG, props={'dialog': PriorityDialog}), + ] + + GenericOptionPage.__init__(self, account, parent, options) + + +class ZeroConfPage(GenericOptionPage): + def __init__(self, account, parent=None): + + options = [ + Option(OptionKind.DIALOG, _('Credentials'), + OptionType.DIALOG, props={'dialog': CredentialsDialog}), + + Option(OptionKind.SWITCH, _('Connect on startup'), + OptionType.ACCOUNT_CONFIG, 'autoconnect', + desc=_('Use environment variable')), + + Option(OptionKind.SWITCH, _('Save conversations for all contacts'), + OptionType.ACCOUNT_CONFIG, 'no_log_for', + desc=_('Store conversations on the harddrive')), + + Option(OptionKind.SWITCH, _('Global Status'), + OptionType.ACCOUNT_CONFIG, 'sync_with_global_status', + desc=_('Synchronize the status of all accounts')), + + Option(OptionKind.GPG, _('OpenPGP Key'), + OptionType.DIALOG, props={'dialog': None}), + ] + + GenericOptionPage.__init__(self, account, parent, options) + + +class CredentialsDialog(OptionsDialog): + def __init__(self, account, parent): + + options = [ + Option(OptionKind.ENTRY, _('First Name'), + OptionType.ACCOUNT_CONFIG, 'zeroconf_first_name'), + + Option(OptionKind.ENTRY, _('Last Name'), + OptionType.ACCOUNT_CONFIG, 'zeroconf_last_name'), + + Option(OptionKind.ENTRY, _('Jabber ID'), + OptionType.ACCOUNT_CONFIG, 'zeroconf_jabber_id'), + + Option(OptionKind.ENTRY, _('Email'), + OptionType.ACCOUNT_CONFIG, 'zeroconf_email'), + ] + + OptionsDialog.__init__(self, parent, _('Credential Options'), + Gtk.DialogFlags.MODAL, options, account) + + +class PriorityDialog(OptionsDialog): + def __init__(self, account, parent): + + neg_priority = app.config.get('enable_negative_priority') + if neg_priority: + range_ = (-128, 127) + else: + range_ = (0, 127) + + options = [ + Option(OptionKind.SWITCH, _('Adjust to status'), + OptionType.ACCOUNT_CONFIG, 'adjust_priority_with_status', + 'adjust'), + + Option(OptionKind.SPIN, _('Priority'), + OptionType.ACCOUNT_CONFIG, 'priority', + enabledif=('adjust', False), props={'range_': range_}), + ] + + OptionsDialog.__init__(self, parent, _('Priority'), + Gtk.DialogFlags.MODAL, options, account) + + self.connect('destroy', self.on_destroy) + + def on_destroy(self, *args): + # Update priority + if self.account not in app.connections: + return + show = app.SHOW_LIST[app.connections[self.account].connected] + status = app.connections[self.account].status + app.connections[self.account].change_status(show, status) + + +class CutstomHostnameDialog(OptionsDialog): + def __init__(self, account, parent): + + options = [ + Option(OptionKind.SWITCH, _('Enable'), + OptionType.ACCOUNT_CONFIG, 'use_custom_host', name='custom'), + + Option(OptionKind.ENTRY, _('Hostname'), + OptionType.ACCOUNT_CONFIG, 'custom_host', + enabledif=('custom', True)), + + Option(OptionKind.ENTRY, _('Port'), + OptionType.ACCOUNT_CONFIG, 'custom_port', + enabledif=('custom', True)), + ] + + OptionsDialog.__init__(self, parent, _('Connection Options'), + Gtk.DialogFlags.MODAL, options, account) + + +class CertificateDialog(OptionsDialog): + def __init__(self, account, parent): + + options = [ + Option(OptionKind.FILECHOOSER, _('Client Certificate'), + OptionType.ACCOUNT_CONFIG, 'client_cert', + props={'filefilter': (_('PKCS12 Files'), '*.p12')}), + + Option(OptionKind.SWITCH, _('Encrypted Certificate'), + OptionType.ACCOUNT_CONFIG, 'client_cert_encrypted'), + ] + + OptionsDialog.__init__(self, parent, _('Certificate Options'), + Gtk.DialogFlags.MODAL, options, account) + + +class LoginDialog(OptionsDialog): + def __init__(self, account, parent): + + options = [ + Option(OptionKind.ENTRY, _('Password'), + OptionType.ACCOUNT_CONFIG, 'password', name='password', + enabledif=('savepass', True)), + + Option(OptionKind.SWITCH, _('Save Password'), + OptionType.ACCOUNT_CONFIG, 'savepass', name='savepass'), + + Option(OptionKind.CHANGEPASSWORD, _('Change Password'), + OptionType.DIALOG, callback=self.on_password_change, + props={'dialog': None}), + ] + + OptionsDialog.__init__(self, parent, _('Login Options'), + Gtk.DialogFlags.MODAL, options, account) + + self.connect('destroy', self.on_destroy) + + def on_password_change(self, new_password, data): + self.get_option('password').entry.set_text(new_password) + + def on_destroy(self, *args): + savepass = app.config.get_per('accounts', self.account, 'savepass') + if not savepass: + passwords.save_password(self.account, '') diff --git a/gajim/app_actions.py b/gajim/app_actions.py index a88a75bb7..e718dfa58 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -25,10 +25,12 @@ from gajim.common.exceptions import GajimGeneralException from gi.repository import Gtk import sys import os + from gajim import config from gajim import dialogs from gajim import features_window from gajim import shortcuts_window +from gajim import accounts_window import gajim.plugins.gui from gajim import history_window from gajim import disco @@ -57,10 +59,10 @@ class AppActions(): interface.instances['plugins'] = gajim.plugins.gui.PluginsWindow() def on_accounts(self, action, param): - if 'accounts' in interface.instances: - interface.instances['accounts'].window.present() + if 'accounts' in app.interface.instances: + app.interface.instances['accounts'].present() else: - interface.instances['accounts'] = config.AccountsWindow() + app.interface.instances['accounts'] = accounts_window.AccountsWindow() def on_history_manager(self, action, param): from gajim.history_manager import HistoryManager @@ -131,6 +133,35 @@ class AppActions(): def on_single_message(self, action, param): dialogs.SingleMessageWindow(param.get_string(), action='send') + def on_merge_accounts(self, action, param): + action.set_state(param) + value = param.get_boolean() + app.config.set('mergeaccounts', value) + if len(app.connections) >= 2: # Do not merge accounts if only one active + app.interface.roster.regroup = value + else: + app.interface.roster.regroup = False + app.interface.roster.setup_and_draw_roster() + + def on_use_pgp_agent(self, action, param): + action.set_state(param) + app.config.set('use_gpg_agent', param.get_boolean()) + + def on_add_account(self, action, param): + if 'account_creation_wizard' in app.interface.instances: + app.interface.instances['account_creation_wizard'].window.present() + else: + app.interface.instances['account_creation_wizard'] = \ + config.AccountCreationWizardWindow() + + def on_import_contacts(self, action, param): + account = param.get_string() + if 'import_contacts' in app.interface.instances: + app.interface.instances['import_contacts'].dialog.present() + else: + app.interface.instances['import_contacts'] = \ + dialogs.SynchroniseSelectAccountDialog(account) + # Advanced Actions def on_archiving_preferences(self, action, param): @@ -174,6 +205,13 @@ class AppActions(): interface.instances[account]['xml_console'] = \ dialogs.XMLConsoleWindow(account) + def on_manage_proxies(self, action, param): + if 'manage_proxies' in app.interface.instances: + app.interface.instances['manage_proxies'].window.present() + else: + app.interface.instances['manage_proxies'] = \ + config.ManageProxiesWindow(interface.roster.window) + # Admin Actions def on_set_motd(self, action, param): diff --git a/gajim/common/config.py b/gajim/common/config.py index 4b5f05ae6..f8ad19b2a 100644 --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -325,6 +325,7 @@ class Config: __options_per_key = { 'accounts': ({ 'name': [ opt_str, '', '', True ], + 'account_label': [ opt_str, '', '', False ], 'hostname': [ opt_str, '', '', True ], 'anonymous_auth': [ opt_bool, False ], 'client_cert': [ opt_str, '', '', True ], diff --git a/gajim/common/const.py b/gajim/common/const.py index 63a801b86..8985ced5c 100644 --- a/gajim/common/const.py +++ b/gajim/common/const.py @@ -1,3 +1,34 @@ +from enum import IntEnum, unique +from collections import namedtuple + +Option = namedtuple('Option', 'kind label type value name callback data desc enabledif props') +Option.__new__.__defaults__ = (None,) * len(Option._fields) + +@unique +class OptionKind(IntEnum): + ENTRY = 0 + SWITCH = 1 + SPIN = 2 + ACTION = 3 + LOGIN = 4 + DIALOG = 5 + CALLBACK = 6 + PROXY = 7 + HOSTNAME = 8 + PRIORITY = 9 + FILECHOOSER = 10 + CHANGEPASSWORD = 11 + GPG = 12 + +@unique +class OptionType(IntEnum): + ACCOUNT_CONFIG = 0 + CONFIG = 1 + BOOL = 2 + ACTION = 3 + DIALOG = 4 + + THANKS = u"""\ Alexander Futász Alexander V. Butenko diff --git a/gajim/config.py b/gajim/config.py index de0ac06bc..1245deabf 100644 --- a/gajim/config.py +++ b/gajim/config.py @@ -3158,10 +3158,10 @@ class RemoveAccountWindow: app.app.remove_account_actions(self.account) gui_menu_builder.build_accounts_menu() if 'accounts' in app.interface.instances: - app.interface.instances['accounts'].init_accounts() - app.interface.instances['accounts'].init_account() + app.interface.instances['accounts'].remove_account(self.account) self.window.destroy() + #---------- ManageBookmarksWindow class -------------# class ManageBookmarksWindow: def __init__(self): @@ -3733,7 +3733,7 @@ class AccountCreationWizardWindow: self.account = server i = 1 - while self.account in app.connections: + while self.account in app.config.get_per('accounts'): self.account = server + str(i) i += 1 @@ -3754,7 +3754,7 @@ class AccountCreationWizardWindow: return self.account = server i = 1 - while self.account in app.connections: + while self.account in app.config.get_per('accounts'): self.account = server + str(i) i += 1 @@ -3982,7 +3982,7 @@ class AccountCreationWizardWindow: def on_advanced_button_clicked(self, widget): if 'accounts' in app.interface.instances: - app.interface.instances['accounts'].window.present() + app.interface.instances['accounts'].present() else: app.interface.instances['accounts'] = AccountsWindow() app.interface.instances['accounts'].select_account(self.account) @@ -4083,9 +4083,11 @@ class AccountCreationWizardWindow: app.gajim_optional_features[self.account] = [] app.caps_hash[self.account] = '' helpers.update_optional_features(self.account) + # action must be added before account window is updated + app.app.add_account_actions(self.account) # refresh accounts window if 'accounts' in app.interface.instances: - app.interface.instances['accounts'].init_accounts() + app.interface.instances['accounts'].add_account(self.account) # refresh roster if len(app.connections) >= 2: # Do not merge accounts if only one exists @@ -4093,7 +4095,6 @@ class AccountCreationWizardWindow: else: app.interface.roster.regroup = False app.interface.roster.setup_and_draw_roster() - app.app.add_account_actions(self.account) gui_menu_builder.build_accounts_menu() class ManagePEPServicesWindow: diff --git a/gajim/dialogs.py b/gajim/dialogs.py index 3ae5d6301..8a3739e5e 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -48,6 +48,8 @@ from random import randrange from gajim.common import pep from gajim.common import ged from gajim.common import const +from gajim.options_dialog import OptionsDialog +from gajim.common.const import Option, OptionKind, OptionType try: from gajim import gtkspell @@ -2675,7 +2677,7 @@ class SynchroniseSelectAccountDialog: self.account = account self.xml = gtkgui_helpers.get_gtk_builder('synchronise_select_account_dialog.ui') self.dialog = self.xml.get_object('synchronise_select_account_dialog') - self.dialog.set_transient_for(app.interface.instances['accounts'].window) + self.dialog.set_transient_for(app.interface.instances['accounts']) self.accounts_treeview = self.xml.get_object('accounts_treeview') model = Gtk.ListStore(str, str, bool) self.accounts_treeview.set_model(model) @@ -2731,6 +2733,10 @@ class SynchroniseSelectAccountDialog: return self.dialog.destroy() + @staticmethod + def on_destroy(widget): + del app.interface.instances['import_contacts'] + class SynchroniseSelectContactsDialog: def __init__(self, account, remote_account): self.local_account = account @@ -3323,6 +3329,8 @@ class XMLConsoleWindow(Gtk.Window): setattr(self, obj, self.builder.get_object(obj)) self.set_titlebar(self.headerbar) + jid = app.get_jid_from_account(account) + self.headerbar.set_subtitle(jid) self.set_default_size(600, 600) self.add(self.box) @@ -3434,26 +3442,30 @@ class XMLConsoleWindow(Gtk.Window): def on_filter_options(self, *args): options = [ - SwitchOption('Presence', self.presence, - self.on_option, - 'presence'), - SwitchOption('Message', self.message, - self.on_option, - 'message'), - SwitchOption('Iq', self.iq, - self.on_option, - 'iq'), - SwitchOption('Stream\nManagement', self.stream, - self.on_option, - 'stream'), - SwitchOption('In', self.incoming, - self.on_option, - 'incoming'), - SwitchOption('Out', self.outgoing, - self.on_option, - 'outgoing')] + Option(OptionKind.SWITCH, 'Presence', + OptionType.BOOL, self.presence, + callback=self.on_option, data='presence'), - OptionsDialog(self, 'Filter', options) + Option(OptionKind.SWITCH, 'Message', + OptionType.BOOL, self.message, + callback=self.on_option, data='message'), + + Option(OptionKind.SWITCH, 'Iq', OptionType.BOOL, self.iq, + callback=self.on_option, data='iq'), + + Option(OptionKind.SWITCH, 'Stream\nManagement', + OptionType.BOOL, self.stream, + callback=self.on_option, data='stream'), + + Option(OptionKind.SWITCH, 'In', OptionType.BOOL, self.incoming, + callback=self.on_option, data='incoming'), + + Option(OptionKind.SWITCH, 'Out', OptionType.BOOL, self.outgoing, + callback=self.on_option, data='outgoing'), + ] + + OptionsDialog(self, 'Filter', Gtk.DialogFlags.DESTROY_WITH_PARENT, + options, self.account) def on_clear(self, *args): buffer_ = self.textview.get_buffer().set_text('') @@ -3468,13 +3480,12 @@ class XMLConsoleWindow(Gtk.Window): def on_enable(self, switch, param): self.enabled = switch.get_active() - def on_option(self, switch, param, *user_data): - kind = user_data[0] - setattr(self, kind, switch.get_active()) - value = not switch.get_active() + def on_option(self, value, data): + setattr(self, data, value) + value = not value table = self.textview.get_buffer().get_tag_table() - tag = table.lookup(kind) - if kind in ('incoming', 'outgoing'): + tag = table.lookup(data) + if data in ('incoming', 'outgoing'): if value: tag.set_priority(table.get_size() - 1) else: @@ -3522,58 +3533,6 @@ class XMLConsoleWindow(Gtk.Window): if at_the_end: GLib.idle_add(gtkgui_helpers.scroll_to_end, self.scrolled) - -class OptionsDialog(Gtk.Dialog): - def __init__(self, parent, title, options): - Gtk.Dialog.__init__(self, title, parent, - Gtk.DialogFlags.DESTROY_WITH_PARENT) - self.set_name('OptionsDialog') - self.set_resizable(False) - self.set_default_size(250, -1) - - self.remove(self.get_content_area()) - - listbox = Gtk.ListBox() - listbox.set_hexpand(True) - listbox.set_selection_mode(Gtk.SelectionMode.NONE) - - for option in options: - listbox.add(option) - - self.add(listbox) - - self.show_all() - listbox.connect('row-activated', self.on_row_activated) - - def on_row_activated(self, listbox, row): - row.get_child().set_switch_state() - - -class SwitchOption(Gtk.Grid): - def __init__(self, label, state, callback, *user_data): - Gtk.Grid.__init__(self) - self.set_column_spacing(6) - - label = Gtk.Label(label=label) - label.set_hexpand(True) - label.set_halign(Gtk.Align.START) - - self.switch = Gtk.Switch() - self.switch.set_active(state) - self.switch.connect("notify::active", callback, *user_data) - self.switch.set_hexpand(True) - self.switch.set_halign(Gtk.Align.END) - self.switch.set_valign(Gtk.Align.CENTER) - - self.add(label) - self.add(self.switch) - self.show_all() - - def set_switch_state(self): - state = self.switch.get_active() - self.switch.set_active(not state) - - #Action that can be done with an incoming list of contacts TRANSLATED_ACTION = {'add': _('add'), 'modify': _('modify'), 'remove': _('remove')} @@ -4580,7 +4539,7 @@ class ClientCertChooserDialog(FileChooserDialog): FileChooserDialog.__init__(self, title_text=_('Choose Client Cert #PCKS12'), - transient_for=app.interface.instances['accounts'].window, + transient_for=app.interface.instances['accounts'], action=Gtk.FileChooserAction.OPEN, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK), diff --git a/gajim/gajim.py b/gajim/gajim.py index 269c1c8af..a3dd77267 100644 --- a/gajim/gajim.py +++ b/gajim/gajim.py @@ -319,6 +319,7 @@ class GajimApplication(Gtk.Application): def add_actions(self): ''' Build Application Actions ''' from gajim.app_actions import AppActions + from gajim.common import app action = AppActions(self) self.account_actions = [ @@ -339,12 +340,30 @@ class GajimApplication(Gtk.Application): ('-update-motd', action.on_update_motd, 'online', 's'), ('-delete-motd', action.on_delete_motd, 'online', 's'), ('-activate-bookmark', - action.on_activate_bookmark, 'online', 'a{sv}') + action.on_activate_bookmark, 'online', 'a{sv}'), + ('-import-contacts', action.on_import_contacts, 'online', 's') ] + # General Stateful Actions + + act = Gio.SimpleAction.new_stateful( + 'merge', None, + GLib.Variant.new_boolean(app.config.get('mergeaccounts'))) + act.connect('change-state', action.on_merge_accounts) + self.add_action(act) + + act = Gio.SimpleAction.new_stateful( + 'agent', None, + GLib.Variant.new_boolean(app.config.get('use_gpg_agent'))) + self.add_action(act) + + # General Actions + self.general_actions = [ ('quit', action.on_quit), ('accounts', action.on_accounts), + ('add-account', action.on_add_account), + ('manage-proxies', action.on_manage_proxies), ('bookmarks', action.on_manage_bookmarks), ('history-manager', action.on_history_manager), ('preferences', action.on_preferences), @@ -364,8 +383,7 @@ class GajimApplication(Gtk.Application): act.connect("activate", func) self.add_action(act) - from gajim.common import app - accounts_list = sorted(app.contacts.get_accounts()) + accounts_list = sorted(app.config.get_per('accounts')) if not accounts_list: return if len(accounts_list) > 1: diff --git a/gajim/gtkgui_helpers.py b/gajim/gtkgui_helpers.py index 599b6d92d..6bd46111e 100644 --- a/gajim/gtkgui_helpers.py +++ b/gajim/gtkgui_helpers.py @@ -107,15 +107,16 @@ def add_image_to_button(button, icon_name): button.set_image(img) def get_image_button(icon_name, tooltip, toggle=False): - icon = get_icon_pixmap(icon_name) - image = Gtk.Image() - image.set_from_pixbuf(icon) if toggle: button = Gtk.ToggleButton() + icon = get_icon_pixmap(icon_name) + image = Gtk.Image() + image.set_from_pixbuf(icon) + button.set_image(image) else: - button = Gtk.Button() + button = Gtk.Button.new_from_icon_name( + icon_name, Gtk.IconSize.MENU) button.set_tooltip_text(_(tooltip)) - button.set_image(image) return button GUI_DIR = os.path.join(app.DATA_DIR, 'gui') diff --git a/gajim/options_dialog.py b/gajim/options_dialog.py new file mode 100644 index 000000000..b1f74ab5e --- /dev/null +++ b/gajim/options_dialog.py @@ -0,0 +1,585 @@ +from gi.repository import Gtk, GLib, Gdk, GObject +from gajim.common import app +from gajim.common import passwords +from gajim import gtkgui_helpers +from gajim.common.const import OptionKind, OptionType +from gajim.common.exceptions import GajimGeneralException +from gajim import dialogs + + +class OptionsDialog(Gtk.ApplicationWindow): + def __init__(self, parent, title, flags, options, account): + Gtk.ApplicationWindow.__init__(self) + self.set_application(app.app) + self.set_show_menubar(False) + self.set_title(title) + self.set_transient_for(parent) + self.set_resizable(False) + self.set_default_size(250, -1) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.account = account + if flags == Gtk.DialogFlags.MODAL: + self.set_modal(True) + elif flags == Gtk.DialogFlags.DESTROY_WITH_PARENT: + self.set_destroy_with_parent(True) + + self.listbox = OptionsBox(account) + self.listbox.set_hexpand(True) + self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) + + for option in options: + self.listbox.add_option(option) + self.listbox.update_states() + + self.add(self.listbox) + + self.show_all() + self.listbox.connect('row-activated', self.on_row_activated) + + @staticmethod + def on_row_activated(listbox, row): + row.get_child().on_row_activated() + + def get_option(self, name): + return self.listbox.get_option(name) + + +class OptionsBox(Gtk.ListBox): + def __init__(self, account): + Gtk.ListBox.__init__(self) + self.set_name('OptionsBox') + self.account = account + self.named_options = {} + + self.map = { + OptionKind.SWITCH: SwitchOption, + OptionKind.SPIN: SpinOption, + OptionKind.DIALOG: DialogOption, + OptionKind.ENTRY: EntryOption, + OptionKind.ACTION: ActionOption, + OptionKind.LOGIN: LoginOption, + OptionKind.FILECHOOSER: FileChooserOption, + OptionKind.CALLBACK: CallbackOption, + OptionKind.PROXY: ProxyComboOption, + OptionKind.PRIORITY: PriorityOption, + OptionKind.HOSTNAME: CutstomHostnameOption, + OptionKind.CHANGEPASSWORD: ChangePasswordOption, + OptionKind.GPG: GPGOption, + } + + def add_option(self, option): + if option.props is not None: + listitem = self.map[option.kind]( + self.account, *option[1:-1], **option.props) + else: + listitem = self.map[option.kind](self.account, *option[1:-1]) + listitem.connect('notify::option-value', self.on_option_changed) + if option.name is not None: + self.named_options[option.name] = listitem + self.add(listitem) + + def get_option(self, name): + return self.named_options[name] + + def update_states(self): + values = [] + values.append((None, None)) + for row in self.get_children(): + name = row.get_child().name + if name is None: + continue + value = row.get_child().get_property('option-value') + values.append((name, value)) + + for name, value in values: + for row in self.get_children(): + row.get_child().set_activatable(name, value) + + def on_option_changed(self, widget, *args): + value = widget.get_property('option-value') + for row in self.get_children(): + row.get_child().set_activatable(widget.name, value) + + +class GenericOption(Gtk.Grid): + def __init__(self, account, label, type_, value, + name, callback, data, desc, enabledif): + Gtk.Grid.__init__(self) + self.set_column_spacing(12) + self.set_size_request(-1, 25) + self.callback = callback + self.type_ = type_ + self.value = value + self.data = data + self.label = label + self.account = account + self.name = name + self.enabledif = enabledif + self.option_value = self.get_value() + + description_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=0) + description_box.set_valign(Gtk.Align.CENTER) + + optiontext = Gtk.Label(label=label) + optiontext.set_hexpand(True) + optiontext.set_halign(Gtk.Align.START) + optiontext.set_valign(Gtk.Align.CENTER) + optiontext.set_vexpand(True) + description_box.add(optiontext) + + if desc is not None: + description = Gtk.Label(label=desc) + description.set_name('SubDescription') + description.set_hexpand(True) + description.set_halign(Gtk.Align.START) + description.set_valign(Gtk.Align.CENTER) + description_box.add(description) + + self.add(description_box) + + self.option_box = Gtk.Box(spacing=6) + self.option_box.set_size_request(200, -1) + self.option_box.set_valign(Gtk.Align.CENTER) + self.option_box.set_name('GenericOptionBox') + self.add(self.option_box) + + def do_get_property(self, prop): + if prop.name == 'option-value': + return self.option_value + else: + raise AttributeError('unknown property %s' % prop.name) + + def do_set_property(self, prop, value): + if prop.name == 'option-value': + self.option_value = value + else: + raise AttributeError('unknown property %s' % prop.name) + + def get_value(self): + return self.__get_value(self.type_, self.value, self.account) + + @staticmethod + def __get_value(type_, value, account): + if value is None: + return + if type_ == OptionType.BOOL: + return value + elif type_ == OptionType.CONFIG: + return app.config.get(value) + elif type_ == OptionType.ACCOUNT_CONFIG: + if value == 'password': + return passwords.get_password(account) + elif value == 'no_log_for': + no_log = app.config.get_per( + 'accounts', account, 'no_log_for').split() + return account not in no_log + else: + return app.config.get_per('accounts', account, value) + elif type_ == OptionType.ACTION: + if value.startswith('-'): + return account + value + return value + else: + raise ValueError('Wrong OptionType?') + + def set_value(self, state): + if self.type_ == OptionType.CONFIG: + app.config.set(self.value, state) + if self.type_ == OptionType.ACCOUNT_CONFIG: + if self.value == 'password': + passwords.save_password(self.account, state) + if self.value == 'no_log_for': + self.set_no_log_for(self.account, state) + else: + app.config.set_per('accounts', self.account, self.value, state) + + if self.callback is not None: + self.callback(state, self.data) + + self.set_property('option-value', state) + + @staticmethod + def set_no_log_for(account, jid): + no_log = app.config.get_per('accounts', account, 'no_log_for').split() + if jid and account in no_log: + no_log.remove(account) + elif not jid and account not in no_log: + no_log.append(account) + app.config.set_per('accounts', account, 'no_log_for', ' '.join(no_log)) + + def on_row_activated(self): + raise NotImplementedError + + def set_activatable(self, name, value): + if self.enabledif is None or self.enabledif[0] != name: + return + activatable = (name, value) == self.enabledif + self.get_parent().set_activatable(activatable) + self.set_sensitive(activatable) + + +class SwitchOption(GenericOption): + + __gproperties__ = { + "option-value": (bool, 'Switch Value', '', False, + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args): + GenericOption.__init__(self, *args) + + self.switch = Gtk.Switch() + self.switch.set_active(self.option_value) + self.switch.connect('notify::active', self.on_switch) + self.switch.set_hexpand(True) + self.switch.set_halign(Gtk.Align.END) + self.switch.set_valign(Gtk.Align.CENTER) + + self.option_box.add(self.switch) + + self.show_all() + + def on_row_activated(self): + state = self.switch.get_active() + self.switch.set_active(not state) + + def on_switch(self, switch, *args): + value = switch.get_active() + self.set_value(value) + + +class EntryOption(GenericOption): + + __gproperties__ = { + "option-value": (str, 'Entry Value', '', '', + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args): + GenericOption.__init__(self, *args) + + self.entry = Gtk.Entry() + self.entry.set_text(str(self.option_value)) + self.entry.connect('notify::text', self.on_text_change) + self.entry.set_valign(Gtk.Align.CENTER) + + if self.value == 'password': + self.entry.set_invisible_char('*') + self.entry.set_visibility(False) + + self.option_box.pack_end(self.entry, True, True, 0) + + self.show_all() + + def on_text_change(self, *args): + text = self.entry.get_text() + self.set_value(text) + + def on_row_activated(self): + self.entry.grab_focus() + + +class DialogOption(GenericOption): + + __gproperties__ = { + "option-value": (str, 'Dummy', '', '', + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args, dialog): + GenericOption.__init__(self, *args) + self.dialog = dialog + + self.option_value = Gtk.Label() + self.option_value.set_text(self.get_option_value()) + self.option_value.set_halign(Gtk.Align.END) + self.option_box.pack_start(self.option_value, True, True, 0) + + self.show_all() + + def show_dialog(self, parent): + if self.dialog: + dialog = self.dialog(self.account, parent) + dialog.connect('destroy', self.on_destroy) + + def on_destroy(self, *args): + self.option_value.set_text(self.get_option_value()) + + def get_option_value(self): + self.option_value.hide() + return '' + + def on_row_activated(self): + self.show_dialog(self.get_toplevel()) + + +class SpinOption(GenericOption): + + __gproperties__ = { + "option-value": (int, 'Priority', '', -128, 127, 0, + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args, range_): + GenericOption.__init__(self, *args) + + lower, upper = range_ + adjustment = Gtk.Adjustment(0, lower, upper, 1, 10, 0) + + self.spin = Gtk.SpinButton() + self.spin.set_adjustment(adjustment) + self.spin.set_numeric(True) + self.spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID) + self.spin.set_value(self.option_value) + self.spin.set_halign(Gtk.Align.END) + self.spin.set_valign(Gtk.Align.CENTER) + self.spin.connect('notify::value', self.on_value_change) + + self.option_box.pack_start(self.spin, True, True, 0) + + self.show_all() + + def on_row_activated(self): + self.spin.grab_focus() + + def on_value_change(self, spin, *args): + value = spin.get_value_as_int() + self.set_value(value) + + +class FileChooserOption(GenericOption): + + __gproperties__ = { + "option-value": (str, 'Certificate Path', '', '', + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args, filefilter): + GenericOption.__init__(self, *args) + + button = Gtk.FileChooserButton(self.label, Gtk.FileChooserAction.OPEN) + button.set_halign(Gtk.Align.END) + + # GTK Bug: The FileChooserButton expands without limit + # get the label and use set_max_wide_chars() + label = button.get_children()[0].get_children()[0].get_children()[1] + label.set_max_width_chars(20) + + if filefilter: + name, pattern = filefilter + filter_ = Gtk.FileFilter() + filter_.set_name(name) + filter_.add_pattern(pattern) + button.add_filter(filter_) + button.set_filter(filter_) + + filter_ = Gtk.FileFilter() + filter_.set_name(_('All files')) + filter_.add_pattern('*') + button.add_filter(filter_) + + if self.option_value: + button.set_filename(self.option_value) + button.connect('selection-changed', self.on_select) + + clear_button = gtkgui_helpers.get_image_button( + 'edit-clear-all-symbolic', 'Clear File') + clear_button.connect('clicked', lambda *args: button.unselect_all()) + self.option_box.pack_start(button, True, True, 0) + self.option_box.pack_start(clear_button, False, False, 0) + + self.show_all() + + def on_select(self, filechooser): + self.set_value(filechooser.get_filename() or '') + + def on_row_activated(self): + pass + + +class CallbackOption(GenericOption): + + __gproperties__ = { + "option-value": (str, 'Dummy', '', '', + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args, callback): + GenericOption.__init__(self, *args) + self.callback = callback + self.show_all() + + def on_row_activated(self): + self.callback() + + +class ActionOption(GenericOption): + + __gproperties__ = { + "option-value": (str, 'Dummy', '', '', + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args, action_args): + GenericOption.__init__(self, *args) + self.action = gtkgui_helpers.get_action(self.option_value) + self.variant = GLib.Variant.new_string(action_args) + self.on_enable() + + self.show_all() + self.action.connect('notify::enabled', self.on_enable) + + def on_enable(self, *args): + self.set_sensitive(self.action.get_enabled()) + + def on_row_activated(self): + self.action.activate(self.variant) + + +class LoginOption(DialogOption): + def __init__(self, *args, **kwargs): + DialogOption.__init__(self, *args, **kwargs) + self.option_value.set_selectable(True) + + def get_option_value(self): + jid = app.get_jid_from_account(self.account) + return jid + + def set_activatable(self, name, value): + DialogOption.set_activatable(self, name, value) + anonym = app.config.get_per('accounts', self.account, 'anonymous_auth') + self.get_parent().set_activatable(not anonym) + + +class ProxyComboOption(GenericOption): + + __gproperties__ = { + "option-value": (str, 'Proxy', '', '', + GObject.ParamFlags.READWRITE),} + + def __init__(self, *args): + GenericOption.__init__(self, *args) + + self.combo = Gtk.ComboBoxText() + self.update_values() + + self.combo.connect('changed', self.on_value_change) + self.combo.set_valign(Gtk.Align.CENTER) + + button = gtkgui_helpers.get_image_button( + 'preferences-system-symbolic', 'Manage Proxies') + button.set_action_name('app.manage-proxies') + button.set_valign(Gtk.Align.CENTER) + + self.option_box.pack_start(self.combo, True, True, 0) + self.option_box.pack_start(button, False, True, 0) + self.show_all() + + def update_values(self): + proxies = app.config.get_per('proxies') + proxies.insert(0, _('None')) + self.combo.remove_all() + for index, value in enumerate(proxies): + self.combo.insert_text(-1, value) + if value == self.option_value or index == 0: + self.combo.set_active(index) + + def on_value_change(self, combo): + self.set_value(combo.get_active_text()) + + def on_row_activated(self): + pass + + +class PriorityOption(DialogOption): + def __init__(self, *args, **kwargs): + DialogOption.__init__(self, *args, **kwargs) + + def get_option_value(self): + adjust = app.config.get_per( + 'accounts', self.account, 'adjust_priority_with_status') + if adjust: + return _('Adjust to Status') + + priority = app.config.get_per('accounts', self.account, 'priority') + return str(priority) + + +class CutstomHostnameOption(DialogOption): + def __init__(self, *args, **kwargs): + DialogOption.__init__(self, *args, **kwargs) + + def get_option_value(self): + custom = app.config.get_per('accounts', self.account, 'use_custom_host') + return _('On') if custom else _('Off') + + +class ChangePasswordOption(DialogOption): + def __init__(self, *args, **kwargs): + DialogOption.__init__(self, *args, **kwargs) + + def show_dialog(self, parent): + try: + self.change_dialog = dialogs.ChangePasswordDialog( + self.account, self.on_changed, parent) + except GajimGeneralException: + return + self.change_dialog.dialog.set_modal(True) + + def on_changed(self, new_password): + if new_password is not None: + app.connections[self.account].change_password(new_password) + self.set_value(new_password) + + def set_activatable(self, name, value): + activatable = False + if self.account in app.connections: + con = app.connections[self.account] + activatable = con.connected >= 2 and con.register_supported + self.get_parent().set_activatable(activatable) + + +class GPGOption(DialogOption): + def __init__(self, *args, **kwargs): + DialogOption.__init__(self, *args, **kwargs) + + def show_dialog(self, parent): + secret_keys = app.connections[self.account].ask_gpg_secrete_keys() + secret_keys[_('None')] = _('None') + + if not secret_keys: + dialogs.ErrorDialog( + _('Failed to get secret keys'), + _('There is no OpenPGP secret key available.'), + transient_for=parent) + return + + dialog = dialogs.ChooseGPGKeyDialog( + _('OpenPGP Key Selection'), _('Choose your OpenPGP key'), + secret_keys, self.on_key_selected, transient_for=parent) + dialog.window.connect('destroy', self.on_destroy) + + def on_key_selected(self, keyID): + if keyID is None: + return + keyid_new, keyname_new = keyID + + keyid = app.config.get_per('accounts', self.account, 'keyid') + + if keyid_new == _('None'): + if keyid == '': + return + app.config.set_per('accounts', self.account, 'keyname', '') + app.config.set_per('accounts', self.account, 'keyid', '') + else: + if keyid == keyid_new: + return + app.config.set_per( + 'accounts', self.account, 'keyname', keyname_new) + app.config.set_per( + 'accounts', self.account, 'keyid', keyid_new) + + def get_option_value(self): + keyid = app.config.get_per('accounts', self.account, 'keyid') + keyname = app.config.get_per('accounts', self.account, 'keyname') + if keyid is not None: + return '\n'.join((keyid, keyname)) + return '' + + def set_activatable(self, name, value): + active = self.account in app.connections + self.get_parent().set_activatable(app.HAVE_GPG and active) diff --git a/gajim/roster_window.py b/gajim/roster_window.py index e07a7f354..b7f70cf5e 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -1052,7 +1052,8 @@ class RosterWindow: account_name = _('Merged accounts') accounts = [] else: - account_name = account + acclabel = app.config.get_per('accounts', account, 'account_label') + account_name = acclabel or account accounts = [account] if account in self.collapsed_rows and \ @@ -3229,7 +3230,7 @@ class RosterWindow: def on_edit_account(self, widget, account): if 'accounts' in app.interface.instances: - app.interface.instances['accounts'].window.present() + app.interface.instances['accounts'].present() else: app.interface.instances['accounts'] = config.AccountsWindow() app.interface.instances['accounts'].select_account(account)