[Sugar-devel] [PATCH sugar v5 2/3] Add proxy configuration support to Network Control Panel

Sascha Silbe silbe at activitycentral.com
Tue Aug 7 09:32:35 EDT 2012


Both individual users and deployments need to be able to set a proxy for
Sugar and activities to use. While we'd like the system to work that all
out automatically (e.g. using WPAD [1]), this often isn't possible. Common
reasons include legacy ("inherited") setups and network uplinks simply being
out of control of the user respectively deployment.

The existing Network Control Panel is enhanced by adding a new section for the
proxy settings. For consistency between Sugar and Gnome, the basic layout of
the Gnome 3 proxy settings has been mirrored: A combo box allows the user to
select how the proxy setting should be determined (None=direct connection,
Automatic=WPAD or PAC, Manual=enter host names and ports for each protocol).
Based on which method was selected, additional configuration options are
presented to the user.

The settings are stored via gconf, using the same keys as Gnome 2 [2].

This implements the Proxy Settings Feature [3].

[1] https://en.wikipedia.org/wiki/Web_Proxy_Autodiscovery_Protocol
[2] http://people.gnome.org/~bmsmith/gconf-docs/C/gnome-vfs.html
[3] https://wiki.sugarlabs.org/go/Features/Proxy_Settings

Signed-off-by: Sascha Silbe <silbe at activitycentral.com>
---
v4->v5: dropped set_alignment() calls on WrappedLabel instances, they
        don't have any effect and would only cause confusion.
v3->v4: completely reworked to turn it into a new section in the
        Network Control Panel and mirroring Gnome 3 design, rather
        than a separate Control Panel mirroring Gnome 2 design.

 extensions/cpsection/network/view.py |  513 +++++++++++++++++++++++++++++++++-
 1 file changed, 508 insertions(+), 5 deletions(-)

diff --git a/extensions/cpsection/network/view.py b/extensions/cpsection/network/view.py
index 6298f5c..e3dd48d 100644
--- a/extensions/cpsection/network/view.py
+++ b/extensions/cpsection/network/view.py
@@ -13,10 +13,13 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+from gettext import gettext as _
+import logging
 
-import gtk
+import gconf
 import gobject
-from gettext import gettext as _
+import gtk
+import pango
 
 from sugar.graphics import style
 
@@ -53,6 +56,379 @@ class WrappedLabel(gtk.Label):
         widget.set_size_request(rect.width, -1)
 
 
+class GConfMixin(object):
+    """Mix-in class for GTK widgets backed by GConf
+
+    It is the callers responsibility to call GConfClient.add_dir() for the
+    GConf directory containing the key.
+    """
+
+    def __init__(self, gconf_key, widget=None, signal='changed'):
+        self._timeout_id = None
+        self._gconf_key = gconf_key
+        client = gconf.client_get_default()
+        self._notify_id = client.notify_add(gconf_key, self.__gconf_notify_cb)
+        initial_value = self._get_gconf_value()
+        self._undo_value = initial_value
+        self.set_value_from_gconf(initial_value)
+        widget = widget or self
+        widget.connect(signal, self.__changed_cb)
+
+    def undo(self):
+        """Revert to original value if modified"""
+        if not self.changed:
+            return
+
+        logging.debug('Reverting %r to %r', self._gconf_key, self._undo_value)
+        self._set_gconf_value(self._undo_value)
+
+    def get_value_for_gconf(self):
+        """
+        Return the current value of the widget in a format suitable for GConf
+
+        MUST be implemented by subclasses.
+        """
+        raise NotImplementedError()
+
+    def set_value_from_gconf(self, value):
+        """
+        Set the current value of the widget based on a value from GConf
+
+        MUST be implemented by subclasses.
+        """
+        raise NotImplementedError()
+
+    def __changed_cb(self, widget):
+        if self._timeout_id is not None:
+            gobject.source_remove(self._timeout_id)
+
+        self._timeout_id = gobject.timeout_add(_APPLY_TIMEOUT, self._commit,
+                                               widget)
+
+    def __gconf_notify_cb(self, client, transaction_id_, entry, user_data_):
+        new_value = _gconf_value_to_python(entry.value)
+        self.set_value_from_gconf(new_value)
+
+    def _commit(self, widget):
+        new_value = self.get_value_for_gconf()
+        logging.debug('Setting %r to %r', self._gconf_key, new_value)
+
+        widget.handler_block_by_func(self.__changed_cb)
+        try:
+            self._set_gconf_value(new_value)
+        finally:
+            widget.handler_unblock_by_func(self.__changed_cb)
+
+    def _set_gconf_value(self, new_value):
+        client = gconf.client_get_default()
+        gconf_type = client.get(self._gconf_key).type
+        if gconf_type == gconf.VALUE_STRING:
+            client.set_string(self._gconf_key, new_value)
+        elif gconf_type == gconf.VALUE_INT:
+            client.set_int(self._gconf_key, new_value)
+        elif gconf_type == gconf.VALUE_FLOAT:
+            client.set_float(self._gconf_key, new_value)
+        elif gconf_type == gconf.VALUE_BOOL:
+            client.set_bool(self._gconf_key, new_value)
+        elif gconf_type == gconf.VALUE_LIST:
+            list_type = client.get(self._gconf_key).get_list_type()
+            client.set_list(self._gconf_key, list_type, new_value)
+        else:
+            raise TypeError('Cannot store %r in GConf' % (new_value, ))
+
+    def _get_gconf_value(self):
+        client = gconf.client_get_default()
+        return _gconf_value_to_python(client.get(self._gconf_key))
+
+
+class GConfEntry(gtk.Entry, GConfMixin):
+    """Text entry backed by GConf
+
+    It is the callers responsibility to call GConfClient.add_dir() for the
+    GConf directory containing the key.
+    """
+
+    def __init__(self, gconf_key):
+        gtk.Entry.__init__(self)
+        GConfMixin.__init__(self, gconf_key)
+
+    def get_value_for_gconf(self):
+        return self.props.text
+
+    def set_value_from_gconf(self, value):
+        self.props.text = value
+
+
+class GConfIntegerSpinButton(gtk.SpinButton, GConfMixin):
+    """Integer SpinButton backed by GConf
+
+    It is the callers responsibility to call GConfClient.add_dir() for the
+    GConf directory containing the key.
+    """
+
+    def __init__(self, gconf_key, adjustment, climb_rate=0):
+        gtk.SpinButton.__init__(self, adjustment, climb_rate=climb_rate)
+        GConfMixin.__init__(self, gconf_key)
+
+    def get_value_for_gconf(self):
+        return self.get_value_as_int()
+
+    def set_value_from_gconf(self, value):
+        self.set_value(value)
+
+
+class GConfStringListEntry(GConfEntry):
+    """Text entry backed by a GConf list of strings"""
+
+    def __init__(self, gconf_key, separator=','):
+        self._separator = separator
+        GConfEntry.__init__(self, gconf_key)
+
+    def get_value_for_gconf(self):
+        entries = self.props.text.split(self._separator)
+        return [entry for entry in entries if entry]
+
+    def set_value_from_gconf(self, value):
+        self.props.text = self._separator.join(value)
+
+
+class SettingBox(gtk.HBox):
+    """
+    Base class for "lines" on the screen representing configuration settings
+    """
+
+    def __init__(self, name, size_group=None):
+        gtk.HBox.__init__(self, spacing=style.DEFAULT_SPACING)
+        self.label = gtk.Label(name)
+        self.label.modify_fg(gtk.STATE_NORMAL,
+                             style.COLOR_SELECTION_GREY.get_gdk_color())
+        self.label.set_alignment(1, 0.5)
+        self.label.show()
+        self.pack_start(self.label, expand=False)
+
+        if size_group is not None:
+            size_group.add_widget(self.label)
+
+
+class GConfStringSettingBox(SettingBox):
+    """A configuration line for a GConf string setting"""
+
+    def __init__(self, name, gconf_key, size_group=None):
+        SettingBox.__init__(self, name, size_group=size_group)
+        self.string_entry = GConfEntry(gconf_key)
+        self.string_entry.show()
+        self.pack_start(self.string_entry, expand=True)
+
+    def undo(self):
+        """Revert to original value if modified"""
+        self.string_entry.undo()
+
+    @property
+    def changed(self):
+        return self.string_entry.changed
+
+
+class GConfHostListSettingBox(GConfStringSettingBox):
+    """A configuration line for a host list GConf setting"""
+
+    def __init__(self, name, gconf_key, size_group=None):
+        SettingBox.__init__(self, name, size_group=size_group)
+        self.hosts_entry = GConfStringListEntry(gconf_key)
+        self.hosts_entry.show()
+        self.pack_start(self.hosts_entry, expand=True)
+
+    def undo(self):
+        """Revert to original value if modified"""
+        self.hosts_entry.undo()
+
+
+class GConfHostPortSettingBox(SettingBox):
+    """A configuration line for a combined host name and port GConf setting"""
+
+    def __init__(self, name, host_key, port_key, size_group=None):
+        SettingBox.__init__(self, name, size_group=size_group)
+        self.host_name_entry = GConfEntry(host_key)
+        self.host_name_entry.show()
+        self.pack_start(self.host_name_entry, expand=True)
+
+        # port number 0 means n/a
+        adjustment = gtk.Adjustment(0, 0, 65535, 1, 10)
+        self.port_spin_button = GConfIntegerSpinButton(port_key, adjustment,
+                                                       climb_rate=0.1)
+        self.port_spin_button.show()
+        self.pack_start(self.port_spin_button, expand=False)
+
+    def undo(self):
+        """Revert to original values if modified"""
+        self.host_name_entry.undo()
+        self.port_spin_button.undo()
+
+
+class ExclusiveOptionSetsBox(gtk.VBox):
+    """
+    Container for sets of different settings selected by a top-level setting
+
+    Renders the top level setting as a ComboBox. Only the currently
+    active set is shown on screen.
+    """
+
+    def __init__(self, top_name, option_sets, size_group=None):
+        """Initialize an ExclusiveOptionSetsBox instance
+
+        Arguments:
+
+        top_name -- text label used for the top-level selection
+        option_sets -- list of tuples containing text label and GTK
+                       widget to display for each of the option sets
+        size_group -- optional gtk.SizeGroup to use for the top-level label
+        """
+        gtk.VBox.__init__(self, spacing=style.DEFAULT_SPACING)
+        self.label_size_group = size_group
+        top_box = gtk.HBox(spacing=style.DEFAULT_SPACING)
+        top_box.show()
+        top_label = gtk.Label(top_name)
+        top_label.modify_fg(gtk.STATE_NORMAL,
+                            style.COLOR_SELECTION_GREY.get_gdk_color())
+        top_label.set_alignment(1, 0.5)
+        top_label.show()
+        self.label_size_group.add_widget(top_label)
+        top_box.pack_start(top_label, expand=False)
+
+        model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_OBJECT)
+        self._top_combo_box = gtk.ComboBox(model=model)
+        self._top_combo_box.connect('changed', self.__combo_changed_cb)
+        self._top_combo_box.show()
+
+        cell_renderer = gtk.CellRendererText()
+        cell_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE
+        cell_renderer.props.ellipsize_set = True
+        self._top_combo_box.pack_start(cell_renderer)
+        self._top_combo_box.add_attribute(cell_renderer, 'text', 0)
+        top_box.pack_start(self._top_combo_box, expand=True)
+        self.pack_start(top_box, expand=False)
+
+        self._settings_box = gtk.VBox()
+        self._settings_box.show()
+        self.pack_start(self._settings_box, expand=False)
+
+        for name, box in option_sets:
+            model.append((name, box))
+
+    def __combo_changed_cb(self, combobox):
+        giter = combobox.get_active_iter()
+        new_box = combobox.get_model().get(giter, 1)[0]
+        current_box = self._settings_box.get_children()
+        if current_box:
+            self._settings_box.remove(current_box[0])
+
+        self._settings_box.add(new_box)
+        new_box.show()
+
+
+class GConfExclusiveOptionSetsBox(ExclusiveOptionSetsBox, GConfMixin):
+    """
+    Container for sets of GConf settings based on a top-level setting
+    """
+
+    def __init__(self, top_name, top_gconf_key, option_sets, size_group=None):
+        """Initialize a GConfExclusiveOptionSetsBox instance
+
+        Arguments:
+
+        top_name -- text label used for the top-level selection
+        top_gconf_key -- key for the GConf entry to use for the
+                         top-level selection
+        option_sets -- list of tuples containing text label, matching
+                       GConf value as well as the GTK widget to display
+                       for each of the option sets
+        size_group -- optional gtk.SizeGroup to use for the top-level label
+        """
+        display_sets = [(name, widget) for name, value, widget in option_sets]
+        self._top_mapping = dict([(name, value)
+                                  for name, value, widget in option_sets])
+        ExclusiveOptionSetsBox.__init__(self, top_name, display_sets,
+                                        size_group=size_group)
+        GConfMixin.__init__(self, top_gconf_key, self._top_combo_box)
+
+    def get_value_for_gconf(self):
+        giter = self._top_combo_box.get_active_iter()
+        if giter is None:
+            return None
+
+        name = self._top_combo_box.get_model().get(giter, 0)[0]
+        return self._top_mapping[name]
+
+    def set_value_from_gconf(self, value):
+        for idx, (name, widget_) in enumerate(self._top_combo_box.get_model()):
+            if self._top_mapping[name] == value:
+                self._top_combo_box.set_active(idx)
+                return
+
+        raise ValueError('Invalid value %r' % (value, ))
+
+
+class OptionalSettingsBox(gtk.VBox):
+    """
+    Container for settings (de)activated by a top-level setting
+
+    Renders the top level setting as a CheckButton. The settings are only
+    shown on screen if the top-level setting is enabled.
+    """
+
+    def __init__(self, top_name, options):
+        """Initialize an OptionalSettingsBox instance
+
+        Arguments:
+
+        top_name -- text label used for the top-level selection
+        options -- list of GTK widgets to display for each of the options
+        """
+        gtk.VBox.__init__(self, spacing=style.DEFAULT_SPACING)
+        self._top_check_button = gtk.CheckButton()
+        self._top_check_button.props.label = top_name
+        self._top_check_button.connect('toggled', self.__button_changed_cb)
+        self._top_check_button.show()
+        self.pack_start(self._top_check_button, expand=True)
+
+        self._settings_box = gtk.VBox(spacing=style.DEFAULT_SPACING)
+        self.pack_start(self._settings_box, expand=False)
+
+        for box in options:
+            self._settings_box.pack_start(box)
+
+    def __button_changed_cb(self, check_button):
+        if check_button.get_active():
+            self._settings_box.show()
+        else:
+            self._settings_box.hide()
+
+
+class GConfOptionalSettingsBox(OptionalSettingsBox, GConfMixin):
+    """
+    Container for GConf settings (de)activated by a top-level setting
+    """
+
+    def __init__(self, top_name, top_gconf_key, options):
+        """Initialize a GConfExclusiveOptionSetsBox instance
+
+        Arguments:
+
+        top_name -- text label used for the top-level selection
+        top_gconf_key -- key for the GConf entry to use for the
+                         top-level selection
+        options -- list of  GTK widgets to display for each of the options
+        """
+        OptionalSettingsBox.__init__(self, top_name, options)
+        GConfMixin.__init__(self, top_gconf_key, self._top_check_button,
+                            signal='toggled')
+
+    def get_value_for_gconf(self):
+        return self._top_check_button.get_active()
+
+    def set_value_from_gconf(self, value):
+        self._top_check_button.set_active(value)
+
+
 class Network(SectionView):
     def __init__(self, model, alerts):
         SectionView.__init__(self)
@@ -65,6 +441,11 @@ class Network(SectionView):
         self._jabber_change_handler = None
         self._radio_change_handler = None
         self._network_configuration_reset_handler = None
+        self._undo_objects = []
+
+        client = gconf.client_get_default()
+        client.add_dir('/system/http_proxy', gconf.CLIENT_PRELOAD_ONELEVEL)
+        client.add_dir('/system/proxy', gconf.CLIENT_PRELOAD_ONELEVEL)
 
         self.set_border_width(style.DEFAULT_SPACING * 2)
         self.set_spacing(style.DEFAULT_SPACING)
@@ -90,7 +471,6 @@ class Network(SectionView):
 
         radio_info = WrappedLabel(_('Turn off the wireless radio to save'
                                     ' battery life'))
-        radio_info.set_alignment(0, 0)
         radio_info.show()
         box_wireless.pack_start(radio_info, expand=False)
 
@@ -118,7 +498,6 @@ class Network(SectionView):
 
         history_info = WrappedLabel(_('Discard network history if you have'
                                       ' trouble connecting to the network'))
-        history_info.set_alignment(0, 0)
         history_info.show()
         box_wireless.pack_start(history_info, expand=False)
 
@@ -152,7 +531,6 @@ class Network(SectionView):
                                      " server will be able to see each other,"
                                      " even when they aren't on the same "
                                      " network."))
-        server_info.set_alignment(0, 0)
         box_mesh.pack_start(server_info, expand=False)
         server_info.show()
 
@@ -191,6 +569,12 @@ class Network(SectionView):
         workspace.pack_start(box_mesh, expand=False)
         box_mesh.show()
 
+        proxy_separator = gtk.HSeparator()
+        workspace.pack_start(proxy_separator, False)
+        proxy_separator.show()
+
+        self._add_proxy_section(workspace)
+
         scrolled = gtk.ScrolledWindow()
         scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
         scrolled.add_with_viewport(workspace)
@@ -199,6 +583,106 @@ class Network(SectionView):
 
         self.setup()
 
+    def _add_proxy_section(self, workspace):
+        proxy_title = gtk.Label(_('Proxy'))
+        proxy_title.set_alignment(0, 0)
+        proxy_title.show()
+        workspace.pack_start(proxy_title, expand=False)
+
+        proxy_box = gtk.VBox()
+        proxy_box.set_border_width(style.DEFAULT_SPACING * 2)
+        proxy_box.set_spacing(style.DEFAULT_SPACING)
+        proxy_box.show()
+
+        workspace.pack_start(proxy_box)
+
+        size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+        automatic_proxy_box = gtk.VBox()
+        automatic_proxy_box.set_spacing(style.DEFAULT_SPACING)
+
+        url_box = GConfStringSettingBox(_('Configuration URL:'),
+                                        '/system/proxy/autoconfig_url',
+                                        size_group)
+        url_box.show()
+        automatic_proxy_box.pack_start(url_box)
+        self._undo_objects.append(url_box)
+
+        wpad_help_text = _('Web Proxy Autodiscovery (WPAD) is used when a'
+                           ' Configuration URL is not provided. This is not'
+                           ' recommended for untrusted public networks.')
+        automatic_proxy_help = WrappedLabel(wpad_help_text)
+        automatic_proxy_help.show()
+        automatic_proxy_box.pack_start(automatic_proxy_help)
+
+        manual_proxy_box = gtk.VBox()
+        manual_proxy_box.set_spacing(style.DEFAULT_SPACING)
+
+        http_box = GConfHostPortSettingBox(_('HTTP Proxy:'),
+                                           '/system/http_proxy/host',
+                                           '/system/http_proxy/port',
+                                           size_group)
+        http_box.show()
+        manual_proxy_box.pack_start(http_box)
+        self._undo_objects.append(http_box)
+
+        user_name_box = GConfStringSettingBox(_('Username:'),
+            '/system/http_proxy/authentication_user', size_group)
+        user_name_box.show()
+        self._undo_objects.append(user_name_box)
+
+        password_box = GConfStringSettingBox(_('Password:'),
+            '/system/http_proxy/authentication_password', size_group)
+        password_box.show()
+        self._undo_objects.append(password_box)
+
+        auth_box = GConfOptionalSettingsBox(_('Use authentication'),
+            '/system/http_proxy/use_authentication',
+            [user_name_box, password_box])
+        auth_box.show()
+        manual_proxy_box.pack_start(auth_box)
+        self._undo_objects.append(auth_box)
+
+        https_box = GConfHostPortSettingBox(_('HTTPS Proxy:'),
+                                            '/system/proxy/secure_host',
+                                            '/system/proxy/secure_port',
+                                            size_group)
+        https_box.show()
+        manual_proxy_box.pack_start(https_box)
+        self._undo_objects.append(https_box)
+
+        ftp_box = GConfHostPortSettingBox(_('FTP Proxy:'),
+                                          '/system/proxy/ftp_host',
+                                          '/system/proxy/ftp_port',
+                                          size_group)
+        ftp_box.show()
+        manual_proxy_box.pack_start(ftp_box)
+        self._undo_objects.append(ftp_box)
+
+        socks_box = GConfHostPortSettingBox(_('SOCKS Proxy:'),
+                                            '/system/proxy/socks_host',
+                                            '/system/proxy/socks_port',
+                                            size_group)
+        socks_box.show()
+        manual_proxy_box.pack_start(socks_box)
+        self._undo_objects.append(socks_box)
+
+        option_sets = [('None', 'none', gtk.VBox()),
+                       ('Automatic', 'auto', automatic_proxy_box),
+                       ('Manual', 'manual', manual_proxy_box)]
+        option_sets_box = GConfExclusiveOptionSetsBox(_('Method:'),
+                                                      '/system/proxy/mode',
+                                                      option_sets, size_group)
+        option_sets_box.show()
+        proxy_box.pack_start(option_sets_box, expand=False)
+        self._undo_objects.append(option_sets_box)
+
+        no_proxy_box = GConfHostListSettingBox(_('Ignored Hosts'),
+            '/system/http_proxy/ignore_hosts', size_group)
+        no_proxy_box.show()
+        proxy_box.pack_start(no_proxy_box, expand=False)
+        self._undo_objects.append(no_proxy_box)
+
     def setup(self):
         self._entry.set_text(self._model.get_jabber())
         try:
@@ -226,6 +710,8 @@ class Network(SectionView):
         self._model.undo()
         self._jabber_alert.hide()
         self._radio_alert.hide()
+        for setting in self._undo_objects:
+            setting.undo()
 
     def _validate(self):
         if self._jabber_valid and self._radio_valid:
@@ -279,3 +765,20 @@ class Network(SectionView):
         self._model.clear_networks()
         if not self._model.have_networks():
             self._clear_history_button.set_sensitive(False)
+
+
+def _gconf_value_to_python(gconf_value):
+    if gconf_value.type == gconf.VALUE_STRING:
+        return gconf_value.get_string()
+    elif gconf_value.type == gconf.VALUE_INT:
+        return gconf_value.get_int()
+    elif gconf_value.type == gconf.VALUE_FLOAT:
+        return gconf_value.get_float()
+    elif gconf_value.type == gconf.VALUE_BOOL:
+        return gconf_value.get_bool()
+    elif gconf_value.type == gconf.VALUE_LIST:
+        return [_gconf_value_to_python(entry)
+                for entry in gconf_value.get_list()]
+    else:
+        raise TypeError("Don't know how to handle GConf value"
+                        " type %r" % (gconf_value.type, ))
-- 
1.7.10.4



More information about the Sugar-devel mailing list