Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/extensions/cpsection
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/cpsection')
-rw-r--r--extensions/cpsection/aboutcomputer/model.py129
-rw-r--r--extensions/cpsection/aboutcomputer/view.py155
-rw-r--r--extensions/cpsection/datetime/model.py47
-rw-r--r--extensions/cpsection/datetime/view.py185
-rw-r--r--extensions/cpsection/modemconfiguration/Makefile.am1
-rw-r--r--extensions/cpsection/modemconfiguration/config.py25
-rwxr-xr-xextensions/cpsection/modemconfiguration/model.py187
-rw-r--r--extensions/cpsection/modemconfiguration/view.py198
-rw-r--r--extensions/cpsection/network/model.py17
-rw-r--r--extensions/cpsection/network/view.py55
10 files changed, 886 insertions, 113 deletions
diff --git a/extensions/cpsection/aboutcomputer/model.py b/extensions/cpsection/aboutcomputer/model.py
index 86d2e3f..5d327ea 100644
--- a/extensions/cpsection/aboutcomputer/model.py
+++ b/extensions/cpsection/aboutcomputer/model.py
@@ -1,4 +1,5 @@
# Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2010 Plan Ceibal <comunidad@plan.ceibal.edu.uy>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -18,9 +19,12 @@
import os
import logging
import re
+import ConfigParser
+import time
import subprocess
from gettext import gettext as _
import errno
+from datetime import datetime
import dbus
@@ -39,6 +43,9 @@ _DMI_DIRECTORY = '/sys/class/dmi/id'
_SN = 'serial-number'
_MODEL = 'openprom/model'
+_XO_1_0_LEASE_PATH = '/security/lease.sig'
+_XO_1_5_LEASE_PATH = '/bootpart/boot/security/lease.sig'
+
_logger = logging.getLogger('ControlPanel - AboutComputer')
_not_available = _('Not available')
@@ -53,6 +60,31 @@ def print_aboutcomputer():
print get_aboutcomputer()
+def _get_lease_path():
+ if os.path.exists(_XO_1_0_LEASE_PATH):
+ return _XO_1_0_LEASE_PATH
+ elif os.path.exists(_XO_1_5_LEASE_PATH):
+ return _XO_1_5_LEASE_PATH
+ else:
+ return ''
+
+
+def get_lease_days():
+ lease_file = _read_file(_get_lease_path())
+ if lease_file is None:
+ return _not_available
+
+ encoded_date = str(str.split(lease_file)[3])
+ expiry_date = datetime.strptime(encoded_date
+ , '%Y%m%dT%H%M%SZ')
+ current_date = datetime.today()
+ days_remaining = (expiry_date - current_date).days
+
+ # TRANS: Do not translate %s
+ str_days_remaining = _('%s days remaining' % str(days_remaining))
+ return str_days_remaining
+
+
def get_serial_number():
serial_no = None
if os.path.exists(os.path.join(_OFW_TREE, _SN)):
@@ -72,7 +104,10 @@ def print_serial_number():
def get_build_number():
- build_no = _read_file('/boot/olpc_build')
+ if os.path.isfile('/boot/olpc_build'):
+ build_no = _read_file('/boot/olpc_build')
+ elif os.path.isfile('/bootpart/olpc_build'):
+ build_no = _read_file('/bootpart/olpc_build')
if build_no is None:
build_no = _read_file('/etc/redhat-release')
@@ -97,6 +132,15 @@ def print_build_number():
print get_build_number()
+def get_model_laptop():
+ from ceibal import laptop
+
+ model_laptop = laptops.get_model_laptop()
+ if model_laptop is None or not model_laptop:
+ model_laptop = _not_available
+ return model_laptop
+
+
def _parse_firmware_number(firmware_no):
if firmware_no is None:
firmware_no = _not_available
@@ -226,3 +270,86 @@ def get_license():
except IOError:
license_text = _not_available
return license_text
+
+
+def get_last_updated_on_field():
+
+ # Get the number of UNIX seconds of the last update date.
+ last_update_unix_seconds = {}
+ try:
+ last_update_unix_seconds = int(os.stat('/var/lib/rpm/Packages').st_mtime)
+ except:
+ msg_str = _('Information not available.')
+ _logger.exception(msg_str)
+ return msg_str
+
+
+ NO_UPDATE_MESSAGE = _('No update yet!')
+
+
+ # Check once again that 'last_update_unix_seconds' is not empty.
+ # You never know !
+ if not last_update_unix_seconds:
+ return NO_UPDATE_MESSAGE
+
+ if int(last_update_unix_seconds) == 1194004800:
+ return NO_UPDATE_MESSAGE
+
+
+ # If we reached here, we have the last-update-time, but it's in
+ # timestamp format.
+ # Using python-subprocess-module (no shell involved), to convert
+ # it into readable date-format; the hack being used (after
+ # removing '-u' option) is the first one mentioned at :
+ # http://www.commandlinefu.com/commands/view/3719/convert-unix-timestamp-to-date
+ environment = os.environ.copy()
+ environment['PATH'] = '%s:/usr/sbin' % (environment['PATH'], )
+
+ last_update_readable_format = {}
+ try:
+ last_update_readable_format = \
+ subprocess.Popen(['date', '-d',
+ '1970-01-01 + ' +
+ str(last_update_unix_seconds) +
+ ' seconds'],
+ stdout=subprocess.PIPE,
+ env=environment).stdout.readlines()[0]
+ except:
+ msg_str = _('Information not available.')
+ _logger.exception(msg_str)
+ return msg_str
+
+ if not last_update_readable_format:
+ return _('Information not available.')
+
+ # Everything should be fine (hopefully :-) )
+ return last_update_readable_format
+
+
+def get_sugar_version():
+ return config.version
+
+
+def get_plazo():
+ from ceibal import env
+ path_plazo = env.get_security_root()
+ try:
+ plazo = _read_file(os.path.join(path_plazo, "blacklist")).split("\n")[0].strip()
+ plazo = time.strftime( "%d-%m-%Y",time.strptime(plazo,'%Y%m%d'))
+ except:
+ plazo = _not_available
+
+ return plazo
+
+def get_act():
+ from ceibal import env
+ path_act = env.get_updates_root()
+ parser = ConfigParser.ConfigParser()
+ salida = parser.read(os.path.join(path_act, "mi_version"))
+ if salida == []:
+ version = _not_available
+ else:
+ version = ''
+ for seccion in parser.sections():
+ version = "%s%s: %s\n" %(version,seccion,parser.get(seccion,'version'))
+ return version
diff --git a/extensions/cpsection/aboutcomputer/view.py b/extensions/cpsection/aboutcomputer/view.py
index f44ca51..1e11301 100644
--- a/extensions/cpsection/aboutcomputer/view.py
+++ b/extensions/cpsection/aboutcomputer/view.py
@@ -23,7 +23,6 @@ from gi.repository import Gdk
from sugar3.graphics import style
-from jarabe import config
from jarabe.controlpanel.sectionview import SectionView
@@ -65,24 +64,51 @@ class AboutComputer(SectionView):
vbox_identity.set_border_width(style.DEFAULT_SPACING * 2)
vbox_identity.set_spacing(style.DEFAULT_SPACING)
- box_identity = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_serial = Gtk.Label(label=_('Serial Number:'))
- label_serial.set_alignment(1, 0)
- label_serial.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_identity.pack_start(label_serial, False, True, 0)
- self._group.add_widget(label_serial)
- label_serial.show()
- label_serial_no = Gtk.Label(label=self._model.get_serial_number())
- label_serial_no.set_alignment(0, 0)
- box_identity.pack_start(label_serial_no, False, True, 0)
- label_serial_no.show()
- vbox_identity.pack_start(box_identity, False, True, 0)
- box_identity.show()
+ self._setup_component_if_applicable(None,
+ _('Serial Number:'),
+ self._model.get_serial_number,
+ vbox_identity)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_lease',
+ _('Lease:'),
+ self._model.get_lease_days,
+ vbox_identity)
self._vbox.pack_start(vbox_identity, False, True, 0)
vbox_identity.show()
+ def _is_feature_to_be_shown(slf, gconf_key):
+ if gconf_key is None:
+ return True
+
+ from gi.repository import GConf
+ client = GConf.Client.get_default()
+
+ return client.get_bool(gconf_key) is True
+
+ def _setup_component_if_applicable(self, gconf_key, key, value_func, packer):
+ if not self._is_feature_to_be_shown(gconf_key):
+ return
+
+ # Now that we do need to show, fetch the value.
+ print value_func
+ value = value_func()
+
+ box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ key_label = Gtk.Label(label=key)
+ key_label.set_alignment(1, 0)
+ key_label.modify_fg(Gtk.StateType.NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+ box.pack_start(key_label, False, True, 0)
+ self._group.add_widget(key_label)
+ key_label.show()
+ value_label = Gtk.Label(label=value)
+ value_label.set_alignment(0, 0)
+ box.pack_start(value_label, False, True, 0)
+ value_label.show()
+ packer.pack_start(box, False, True, 0)
+ box.show()
+
def _setup_software(self):
separator_software = Gtk.HSeparator()
self._vbox.pack_start(separator_software, False, True, 0)
@@ -96,66 +122,45 @@ class AboutComputer(SectionView):
box_software.set_border_width(style.DEFAULT_SPACING * 2)
box_software.set_spacing(style.DEFAULT_SPACING)
- box_build = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_build = Gtk.Label(label=_('Build:'))
- label_build.set_alignment(1, 0)
- label_build.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_build.pack_start(label_build, False, True, 0)
- self._group.add_widget(label_build)
- label_build.show()
- label_build_no = Gtk.Label(label=self._model.get_build_number())
- label_build_no.set_alignment(0, 0)
- box_build.pack_start(label_build_no, False, True, 0)
- label_build_no.show()
- box_software.pack_start(box_build, False, True, 0)
- box_build.show()
-
- box_sugar = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_sugar = Gtk.Label(label=_('Sugar:'))
- label_sugar.set_alignment(1, 0)
- label_sugar.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_sugar.pack_start(label_sugar, False, True, 0)
- self._group.add_widget(label_sugar)
- label_sugar.show()
- label_sugar_ver = Gtk.Label(label=config.version)
- label_sugar_ver.set_alignment(0, 0)
- box_sugar.pack_start(label_sugar_ver, False, True, 0)
- label_sugar_ver.show()
- box_software.pack_start(box_sugar, False, True, 0)
- box_sugar.show()
-
- box_firmware = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_firmware = Gtk.Label(label=_('Firmware:'))
- label_firmware.set_alignment(1, 0)
- label_firmware.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_firmware.pack_start(label_firmware, False, True, 0)
- self._group.add_widget(label_firmware)
- label_firmware.show()
- label_firmware_no = Gtk.Label(label=self._model.get_firmware_number())
- label_firmware_no.set_alignment(0, 0)
- box_firmware.pack_start(label_firmware_no, False, True, 0)
- label_firmware_no.show()
- box_software.pack_start(box_firmware, False, True, 0)
- box_firmware.show()
-
- box_wireless_fw = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- label_wireless_fw = Gtk.Label(label=_('Wireless Firmware:'))
- label_wireless_fw.set_alignment(1, 0)
- label_wireless_fw.modify_fg(Gtk.StateType.NORMAL,
- style.COLOR_SELECTION_GREY.get_gdk_color())
- box_wireless_fw.pack_start(label_wireless_fw, False, True, 0)
- self._group.add_widget(label_wireless_fw)
- label_wireless_fw.show()
- wireless_fw_no = self._model.get_wireless_firmware()
- label_wireless_fw_no = Gtk.Label(label=wireless_fw_no)
- label_wireless_fw_no.set_alignment(0, 0)
- box_wireless_fw.pack_start(label_wireless_fw_no, False, True, 0)
- label_wireless_fw_no.show()
- box_software.pack_start(box_wireless_fw, False, True, 0)
- box_wireless_fw.show()
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_model',
+ _('Model:'),
+ self._model.get_model_laptop,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Build:'),
+ self._model.get_build_number,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Sugar:'),
+ self._model.get_sugar_version,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Firmware:'),
+ self._model.get_firmware_number,
+ box_software)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_wireless_firmware',
+ _('Wireless Firmware:'),
+ self._model.get_wireless_firmware,
+ box_software)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_plazo',
+ _('Plazo:'),
+ self._model.get_plazo,
+ box_software)
+
+ self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_version_de_actual',
+ _('VersiĆ³n de ActualizaciĆ³n:'),
+ self._model.get_act,
+ box_software)
+
+ self._setup_component_if_applicable(None,
+ _('Last Updated On:'),
+ self._model.get_last_updated_on_field,
+ box_software)
self._vbox.pack_start(box_software, False, True, 0)
box_software.show()
diff --git a/extensions/cpsection/datetime/model.py b/extensions/cpsection/datetime/model.py
index c9b4586..f73bef7 100644
--- a/extensions/cpsection/datetime/model.py
+++ b/extensions/cpsection/datetime/model.py
@@ -21,10 +21,57 @@
#
import os
+import logging
+
from gettext import gettext as _
from gi.repository import GConf
_zone_tab = '/usr/share/zoneinfo/zone.tab'
+NTPDATE_PATH = '/usr/sbin/ntpdate'
+NTP_SERVER_CONFIG_FILENAME = '/etc/ntp/step-tickers'
+
+_logger = logging.getLogger('ControlPanel - TimeZone')
+
+
+def is_ntp_servers_config_feature_available():
+ return os.path.exists(NTPDATE_PATH)
+
+
+def get_ntp_servers():
+ servers = []
+
+ # If the file does not exist, return.
+ if not os.path.exists(NTP_SERVER_CONFIG_FILENAME):
+ return servers
+
+ f = open(NTP_SERVER_CONFIG_FILENAME, 'r')
+ for server in f.readlines():
+ servers.append(server.rstrip('\n'))
+ f.close()
+
+ return servers
+
+
+def set_ntp_servers(servers):
+
+ # First remove the old ssid-file, if it exists.
+ if os.path.exists(NTP_SERVER_CONFIG_FILENAME):
+ try:
+ os.remove(NTP_SERVER_CONFIG_FILENAME)
+ except:
+ _logger.exception('Error removing file.')
+ return
+
+ # Do nothing and return, if the values-list is empty
+ if len(servers) == 0:
+ return
+
+ # If we reach here, we have a non-empty ssid-values-list.
+ f = open(NTP_SERVER_CONFIG_FILENAME, 'w')
+ for server in servers:
+ if len(server) > 0:
+ f.write(server + '\n')
+ f.close()
def _initialize():
diff --git a/extensions/cpsection/datetime/view.py b/extensions/cpsection/datetime/view.py
index 64789b4..4ad94ca 100644
--- a/extensions/cpsection/datetime/view.py
+++ b/extensions/cpsection/datetime/view.py
@@ -20,11 +20,130 @@ from gettext import gettext as _
from sugar3.graphics import style
from sugar3.graphics import iconentry
+from sugar3.graphics.icon import Icon
from jarabe.controlpanel.sectionview import SectionView
from jarabe.controlpanel.inlinealert import InlineAlert
+class AddRemoveWidget(Gtk.HBox):
+
+ def __init__(self, label, add_button_clicked_cb,
+ remove_button_clicked_cb, index):
+ Gtk.Box.__init__(self)
+ self.set_homogeneous(False)
+ self.set_spacing(10)
+
+ self._index = index
+ self._add_button_added = False
+ self._remove_button_added = False
+
+ self._entry_box = Gtk.Entry()
+ self._entry_box.set_text(label)
+ self._entry_box.set_width_chars(40)
+ self.pack_start(self._entry_box, False, False, 0)
+ self._entry_box.show()
+
+ add_icon = Icon(icon_name='list-add')
+ self._add_button = Gtk.Button()
+ self._add_button.set_image(add_icon)
+ self._add_button.connect('clicked',
+ add_button_clicked_cb,
+ self)
+
+ remove_icon = Icon(icon_name='list-remove')
+ self._remove_button = Gtk.Button()
+ self._remove_button.set_image(remove_icon)
+ self._remove_button.connect('clicked',
+ remove_button_clicked_cb,
+ self)
+ self.__add_add_button()
+ self.__add_remove_button()
+
+ def _get_index(self):
+ return self._index
+
+ def _set_index(self, value):
+ self._index = value
+
+ def _get_entry(self):
+ return self._entry_box.get_text()
+
+ def __add_add_button(self):
+ self.pack_start(self._add_button, False, False, 0)
+ self._add_button.show()
+ self._add_button_added = True
+
+ def _remove_remove_button_if_not_already(self):
+ if self._remove_button_added:
+ self.__remove_remove_button()
+
+ def __remove_remove_button(self):
+ self.remove(self._remove_button)
+ self._remove_button_added = False
+
+ def _add_remove_button_if_not_already(self):
+ if not self._remove_button_added:
+ self.__add_remove_button()
+
+ def __add_remove_button(self):
+ self.pack_start(self._remove_button, False, False, 0)
+ self._remove_button.show()
+ self._remove_button_added = True
+
+
+class MultiWidget(Gtk.VBox):
+
+ def __init__(self):
+ Gtk.VBox.__init__(self)
+
+ def _add_widget(self, label):
+ new_widget = AddRemoveWidget(label,
+ self.__add_button_clicked_cb,
+ self.__remove_button_clicked_cb,
+ len(self.get_children()))
+ self.add(new_widget)
+ new_widget.show()
+ self.show()
+ self._update_remove_button_statuses()
+
+ def __add_button_clicked_cb(self, add_button,
+ add_button_container):
+ self._add_widget('')
+ self._update_remove_button_statuses()
+
+ def __remove_button_clicked_cb(self, remove_button,
+ remove_button_container):
+ for child in self.get_children():
+ if child._get_index() > remove_button_container._get_index():
+ child._set_index(child._get_index() - 1)
+
+ self.remove(remove_button_container)
+ self._update_remove_button_statuses()
+
+ def _update_remove_button_statuses(self):
+ children = self.get_children()
+
+ # Now, if there is only one entry, remove-button
+ # should not be shown.
+ if len(children) == 1:
+ children[0]._remove_remove_button_if_not_already()
+
+ # Alternatively, if there are more than 1 entries,
+ # remove-button should be shown for all.
+ if len(children) > 1:
+ for child in children:
+ child._add_remove_button_if_not_already()
+
+
+ def _get_entries(self):
+ entries = []
+ for child in self.get_children():
+ entries.append(child._get_entry())
+
+ return entries
+
+
class TimeZone(SectionView):
def __init__(self, model, alerts):
SectionView.__init__(self)
@@ -64,7 +183,10 @@ class TimeZone(SectionView):
self._treeview.set_search_entry(self._entry)
self._treeview.set_search_equal_func(self._search, None)
self._treeview.set_search_column(0)
- self._scrolled_window.add(self._treeview)
+ self._timezone_box = Gtk.VBox()
+ self._scrolled_window.add(self._timezone_box)
+ self._timezone_box.show_all()
+ self._timezone_box.add(self._treeview)
self._treeview.show()
self._timezone_column = Gtk.TreeViewColumn(_('Timezone'))
@@ -74,19 +196,29 @@ class TimeZone(SectionView):
self._timezone_column.set_sort_column_id(0)
self._treeview.append_column(self._timezone_column)
- self.pack_start(self._scrolled_window, True, True, 0)
- self._scrolled_window.show()
+ self._container = Gtk.VBox()
+ self._container.set_homogeneous(False)
+ self._container.pack_start(self._scrolled_window, True, True, 0)
+ self._container.set_spacing(style.DEFAULT_SPACING)
+ self._container.show_all()
+ self.pack_start(self._container, True, True, 0)
self._zone_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
- self.pack_start(self._zone_alert_box, False, False, 0)
-
+ self._timezone_box.pack_start(self._zone_alert_box, False, False, 0)
self._zone_alert = InlineAlert()
self._zone_alert_box.pack_start(self._zone_alert, True, True, 0)
if 'zone' in self.restart_alerts:
self._zone_alert.props.msg = self.restart_msg
self._zone_alert.show()
+
+ # Not showing this, as this hides the selected timezone.
+ # Instead, the alert will anyways be shown when user clicks
+ # on "Ok".
+ #self._zone_alert.show()
self._zone_alert_box.show()
+ self._ntp_ui_setup = False
+
self.setup()
def setup(self):
@@ -102,6 +234,45 @@ class TimeZone(SectionView):
self.needs_restart = False
self._cursor_change_handler = self._treeview.connect( \
'cursor-changed', self.__zone_changed_cd)
+ if self._model.is_ntp_servers_config_feature_available():
+ self.setup_ui_for_ntp_server_config()
+
+ def setup_ui_for_ntp_server_config(self):
+ if self._ntp_ui_setup:
+ return
+ self._ntp_ui_setup = True
+
+ self._ntp_scrolled_window = Gtk.ScrolledWindow()
+ self._ntp_scrolled_window.set_policy(Gtk.PolicyType.NEVER,
+ Gtk.PolicyType.AUTOMATIC)
+ box_ntp_servers_config = Gtk.VBox()
+ box_ntp_servers_config.set_spacing(style.DEFAULT_SPACING)
+
+ separator_ntp_servers_config= Gtk.HSeparator()
+ self._container.pack_start(separator_ntp_servers_config,
+ False, False, 0)
+ separator_ntp_servers_config.show()
+
+ label_ntp_servers_config = Gtk.Label(_('NTP Servers Configuration'))
+ label_ntp_servers_config.set_alignment(0, 0)
+ self._container.pack_start(label_ntp_servers_config,
+ False, False, 0)
+ label_ntp_servers_config.show()
+
+ self._widget_table = MultiWidget()
+ box_ntp_servers_config.pack_start(self._widget_table, False, False, 0)
+ box_ntp_servers_config.show_all()
+
+ self._ntp_scrolled_window.add_with_viewport(box_ntp_servers_config)
+ self._container.pack_start(self._ntp_scrolled_window, True, True, 0)
+ self._ntp_scrolled_window.show_all()
+
+ ntp_servers = self._model.get_ntp_servers()
+ if len(ntp_servers) == 0:
+ self._widget_table._add_widget('')
+ else:
+ for server in ntp_servers:
+ self._widget_table._add_widget(server)
def undo(self):
self._treeview.disconnect(self._cursor_change_handler)
@@ -136,5 +307,7 @@ class TimeZone(SectionView):
self.restart_alerts.append('zone')
self.needs_restart = True
self._zone_alert.props.msg = self.restart_msg
- self._zone_alert.show()
return False
+
+ def perform_accept_actions(self):
+ self._model.set_ntp_servers(self._widget_table._get_entries())
diff --git a/extensions/cpsection/modemconfiguration/Makefile.am b/extensions/cpsection/modemconfiguration/Makefile.am
index 3e2613e..46f8e70 100644
--- a/extensions/cpsection/modemconfiguration/Makefile.am
+++ b/extensions/cpsection/modemconfiguration/Makefile.am
@@ -3,4 +3,5 @@ sugardir = $(pkgdatadir)/extensions/cpsection/modemconfiguration
sugar_PYTHON = \
__init__.py \
model.py \
+ config.py \
view.py
diff --git a/extensions/cpsection/modemconfiguration/config.py b/extensions/cpsection/modemconfiguration/config.py
new file mode 100644
index 0000000..9e27814
--- /dev/null
+++ b/extensions/cpsection/modemconfiguration/config.py
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+# Copyright (C) 2010 Andres Ambrois
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 US
+
+
+PROVIDERS_PATH = "/usr/share/mobile-broadband-provider-info/serviceproviders.xml"
+PROVIDERS_FORMAT_SUPPORTED = "2.0"
+COUNTRY_CODES_PATH = "/usr/share/zoneinfo/iso3166.tab"
+
+GSM_COUNTRY_PATH = '/desktop/sugar/network/gsm/country'
+GSM_PROVIDERS_PATH = '/desktop/sugar/network/gsm/providers'
+GSM_PLAN_PATH = '/desktop/sugar/network/gsm/plan'
diff --git a/extensions/cpsection/modemconfiguration/model.py b/extensions/cpsection/modemconfiguration/model.py
index f457293..e33d881 100755
--- a/extensions/cpsection/modemconfiguration/model.py
+++ b/extensions/cpsection/modemconfiguration/model.py
@@ -19,14 +19,27 @@ import logging
import dbus
from gi.repository import Gtk
+import os
+import locale
+import logging
+import gconf
+
+from xml.etree.cElementTree import ElementTree
+from gettext import gettext as _
+
from jarabe.model import network
+from cpsection.modemconfiguration.config import PROVIDERS_PATH, \
+ PROVIDERS_FORMAT_SUPPORTED, \
+ COUNTRY_CODES_PATH
+
+
def get_connection():
return network.find_gsm_connection()
-def get_modem_settings():
+def get_modem_settings(callback):
modem_settings = {}
connection = get_connection()
if not connection:
@@ -48,6 +61,10 @@ def get_modem_settings():
modem_settings['password'] = gsm_secrets.get('password', '')
modem_settings['pin'] = gsm_secrets.get('pin', '')
+ # sl#3800: We return the settings, via the "_secrets_cb()
+ # method", instead of busy-waiting.
+ callback(modem_settings)
+
def _secrets_err_cb(err):
secrets_call_done[0] = True
if isinstance(err, dbus.exceptions.DBusException) and \
@@ -57,14 +74,11 @@ def get_modem_settings():
logging.error('Error retrieving GSM secrets: %s', err)
# must be called asynchronously as this re-enters the GTK main loop
+ #
+ # sl#3800: We return the settings, via the "_secrets_cb()" method,
+ # instead of busy-waiting.
connection.get_secrets('gsm', _secrets_cb, _secrets_err_cb)
- # wait til asynchronous execution completes
- while not secrets_call_done[0]:
- Gtk.main_iteration()
-
- return modem_settings
-
def _set_or_clear(_dict, key, value):
"""Update a dictionary value for a specific key. If value is None or
@@ -98,3 +112,162 @@ def set_modem_settings(modem_settings):
_set_or_clear(gsm_settings, 'apn', apn)
_set_or_clear(gsm_settings, 'pin', pin)
connection.update_settings(settings)
+
+
+def has_providers_db():
+ if not os.path.isfile(COUNTRY_CODES_PATH):
+ logging.debug("Mobile broadband provider database: Country " \
+ "codes path %s not found.", COUNTRY_CODES_PATH)
+ return False
+ try:
+ tree = ElementTree(file=PROVIDERS_PATH)
+ except (IOError, SyntaxError), e:
+ logging.debug("Mobile broadband provider database: Could not read " \
+ "provider information %s error=%s", PROVIDERS_PATH)
+ return False
+ else:
+ elem = tree.getroot()
+ if elem is None or elem.get('format') != PROVIDERS_FORMAT_SUPPORTED:
+ logging.debug("Mobile broadband provider database: Could not " \
+ "read provider information. %s is wrong format.",
+ elem.get('format'))
+ return False
+ return True
+
+
+class CountryListStore(Gtk.ListStore):
+ COUNTRY_CODE = locale.getdefaultlocale()[0][3:5].lower()
+
+ def __init__(self):
+ Gtk.ListStore.__init__(self, str, object)
+ codes = {}
+ with open(COUNTRY_CODES_PATH) as codes_file:
+ for line in codes_file:
+ if line.startswith('#'):
+ continue
+ code, name = line.split('\t')[:2]
+ codes[code.lower()] = name.strip()
+ etree = ElementTree(file=PROVIDERS_PATH).getroot()
+ self._country_idx = None
+ i = 0
+
+ # This dictionary wil store the values, with "country-name" as
+ # the key, and "country-code" as the value.
+ temp_dict = {}
+
+ for elem in etree.findall('.//country'):
+ code = elem.attrib['code']
+ if code == self.COUNTRY_CODE:
+ self._country_idx = i
+ else:
+ i += 1
+ if code in codes:
+ temp_dict[codes[code]] = elem
+ else:
+ temp_dict[code] = elem
+
+ # Now, sort the list by country-names.
+ country_name_keys = temp_dict.keys()
+ country_name_keys.sort()
+
+ for country_name in country_name_keys:
+ self.append((country_name, temp_dict[country_name]))
+
+ def get_row_providers(self, row):
+ return self[row][1]
+
+ def guess_country_row(self):
+ if self._country_idx is not None:
+ return self._country_idx
+ else:
+ return 0
+
+ def search_index_by_code(self, code):
+ for index in range(0, len(self)):
+ if self[index][0] == code:
+ return index
+ return -1
+
+
+class ProviderListStore(Gtk.ListStore):
+ def __init__(self, elem):
+ Gtk.ListStore.__init__(self, str, object)
+ for provider_elem in elem.findall('.//provider'):
+ apns = provider_elem.findall('.//apn')
+ if not apns:
+ # Skip carriers with CDMA entries only
+ continue
+ self.append((provider_elem.find('.//name').text, apns))
+
+ def get_row_plans(self, row):
+ return self[row][1]
+
+ def guess_providers_row(self):
+ # Simply return the first entry as the default.
+ return 0
+
+ def search_index_by_code(self, code):
+ for index in range(0, len(self)):
+ if self[index][0] == code:
+ return index
+ return -1
+
+
+class PlanListStore(Gtk.ListStore):
+ LANG_NS_ATTR = '{http://www.w3.org/XML/1998/namespace}lang'
+ LANG = locale.getdefaultlocale()[0][:2]
+ DEFAULT_NUMBER = '*99#'
+
+ def __init__(self, elems):
+ Gtk.ListStore.__init__(self, str, object)
+ for apn_elem in elems:
+ plan = {}
+ names = apn_elem.findall('.//name')
+ if names:
+ for name in names:
+ if name.get(self.LANG_NS_ATTR) is None:
+ # serviceproviders.xml default value
+ plan['name'] = name.text
+ elif name.get(self.LANG_NS_ATTR) == self.LANG:
+ # Great! We found a name value for our locale!
+ plan['name'] = name.text
+ break
+ else:
+ plan['name'] = _('Default')
+ plan['apn'] = apn_elem.get('value')
+ user = apn_elem.find('.//username')
+ if user is not None:
+ plan['username'] = user.text
+ else:
+ plan['username'] = ''
+ passwd = apn_elem.find('.//password')
+ if passwd is not None:
+ plan['password'] = passwd.text
+ else:
+ plan['password'] = ''
+
+ plan['number'] = self.DEFAULT_NUMBER
+
+ self.append((plan['name'], plan))
+
+ def get_row_plan(self, row):
+ return self[row][1]
+
+ def guess_plan_row(self):
+ # Simply return the first entry as the default.
+ return 0
+
+ def search_index_by_code(self, code):
+ for index in range(0, len(self)):
+ if self[index][0] == code:
+ return index
+ return -1
+
+
+def get_gconf_setting_string(gconf_key):
+ client = gconf.client_get_default()
+ return client.get_string(gconf_key) or ''
+
+def set_gconf_setting_string(gconf_key, gconf_setting_string_value):
+ client = gconf.client_get_default()
+ client.set_string(gconf_key, gconf_setting_string_value)
diff --git a/extensions/cpsection/modemconfiguration/view.py b/extensions/cpsection/modemconfiguration/view.py
index d5aa399..d218330 100644
--- a/extensions/cpsection/modemconfiguration/view.py
+++ b/extensions/cpsection/modemconfiguration/view.py
@@ -24,6 +24,10 @@ from sugar3.graphics import style
from jarabe.controlpanel.sectionview import SectionView
+from cpsection.modemconfiguration.config import GSM_COUNTRY_PATH, \
+ GSM_PROVIDERS_PATH, \
+ GSM_PLAN_PATH
+
APPLY_TIMEOUT = 1000
@@ -64,6 +68,17 @@ class ModemConfiguration(SectionView):
self.set_border_width(style.DEFAULT_SPACING)
self.set_spacing(style.DEFAULT_SPACING)
self._group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
+ self._combo_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
+
+ scrolled_win = Gtk.ScrolledWindow()
+ scrolled_win.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
+ scrolled_win.show()
+ self.add(scrolled_win)
+
+ main_box = Gtk.VBox(spacing=style.DEFAULT_SPACING)
+ main_box.set_border_width(style.DEFAULT_SPACING)
+ main_box.show()
+ scrolled_win.add_with_viewport(main_box)
explanation = _('You will need to provide the following information'
' to set up a mobile broadband connection to a'
@@ -71,41 +86,85 @@ class ModemConfiguration(SectionView):
self._text = Gtk.Label(label=explanation)
self._text.set_line_wrap(True)
self._text.set_alignment(0, 0)
- self.pack_start(self._text, False, False, 0)
+ main_box.pack_start(self._text, False, False, 0)
self._text.show()
+ if model.has_providers_db():
+ self._upper_box = Gtk.VBox(spacing=style.DEFAULT_SPACING)
+ self._upper_box.set_border_width(style.DEFAULT_SPACING)
+ main_box.pack_start(self._upper_box, True, True, 0)
+ self._upper_box.show()
+
+ # Do not attach any 'change'-handlers for now.
+ # They will be attached (once per combobox), once the
+ # individual combobox is processed at startup.
+ self._country_store = model.CountryListStore()
+ self._country_combo = Gtk.ComboBox(model=self._country_store)
+ self._attach_combobox_widget(_('Country:'),
+ self._country_combo)
+
+ self._providers_combo = Gtk.ComboBox()
+ self._attach_combobox_widget(_('Provider:'),
+ self._providers_combo)
+
+ self._plan_combo = Gtk.ComboBox()
+ self._attach_combobox_widget(_('Plan:'),
+ self._plan_combo)
+
+ separator = Gtk.HSeparator()
+ main_box.pack_start(separator, True, True, 0)
+ separator.show()
+
+ self._lower_box = Gtk.VBox(spacing=style.DEFAULT_SPACING)
+ self._lower_box.set_border_width(style.DEFAULT_SPACING)
+ main_box.pack_start(self._lower_box, True, True, 0)
+ self._lower_box.show()
+
self._username_entry = EntryWithLabel(_('Username:'))
- self._username_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._username_entry.label)
- self.pack_start(self._username_entry, False, True, 0)
- self._username_entry.show()
+ self._attach_entry_widget(self._username_entry)
self._password_entry = EntryWithLabel(_('Password:'))
- self._password_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._password_entry.label)
- self.pack_start(self._password_entry, False, True, 0)
- self._password_entry.show()
+ self._attach_entry_widget(self._password_entry)
self._number_entry = EntryWithLabel(_('Number:'))
- self._number_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._number_entry.label)
- self.pack_start(self._number_entry, False, True, 0)
- self._number_entry.show()
+ self._attach_entry_widget(self._number_entry)
self._apn_entry = EntryWithLabel(_('Access Point Name (APN):'))
- self._apn_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._apn_entry.label)
- self.pack_start(self._apn_entry, False, True, 0)
- self._apn_entry.show()
+ self._attach_entry_widget(self._apn_entry)
self._pin_entry = EntryWithLabel(_('Personal Identity Number (PIN):'))
- self._pin_entry.entry.connect('changed', self.__entry_changed_cb)
- self._group.add_widget(self._pin_entry.label)
- self.pack_start(self._pin_entry, False, True, 0)
- self._pin_entry.show()
+ self._attach_entry_widget(self._pin_entry)
self.setup()
+ def _attach_combobox_widget(self, label_text, combobox_obj):
+ box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ label = Gtk.Label(label_text)
+ self._group.add_widget(label)
+ label.set_alignment(1, 0.5)
+ box.pack_start(label, False, False, 0)
+ label.show()
+
+ self._combo_group.add_widget(combobox_obj)
+ cell = Gtk.CellRendererText()
+ cell.props.xalign = 0.5
+
+ cell.set_property('width-chars', 30)
+
+ combobox_obj.pack_start(cell, True)
+ combobox_obj.add_attribute(cell, 'text', 0)
+
+ box.pack_start(combobox_obj, False, False, 0)
+ combobox_obj.show()
+ self._upper_box.pack_start(box, False, False, 0)
+ box.show()
+
+ def _attach_entry_widget(self, entry_with_label_obj):
+ entry_with_label_obj.entry.connect('changed', self.__entry_changed_cb)
+ self._group.add_widget(entry_with_label_obj.label)
+ self._lower_box.pack_start(entry_with_label_obj, True, True, 0)
+ entry_with_label_obj.show()
+
def undo(self):
self._model.undo()
@@ -113,12 +172,31 @@ class ModemConfiguration(SectionView):
"""Populate an entry with text, without triggering its 'changed'
handler."""
entry = entrywithlabel.entry
- entry.handler_block_by_func(self.__entry_changed_cb)
+
+ # Do not block/unblock the callback functions.
+ #
+ # Thus, the savings will be persisted to the NM settings,
+ # whenever any setting on the UI changes (by user-intervention,
+ # or otherwise).
+ #entry.handler_block_by_func(self.__entry_changed_cb)
entry.set_text(text)
- entry.handler_unblock_by_func(self.__entry_changed_cb)
+ #entry.handler_unblock_by_func(self.__entry_changed_cb)
def setup(self):
- settings = self._model.get_modem_settings()
+ if self._model.has_providers_db():
+ persisted_country = self._model.get_gconf_setting_string(GSM_COUNTRY_PATH)
+ if (self._model.has_providers_db()) and (persisted_country != ''):
+ self._country_combo.set_active(self._country_store.search_index_by_code(persisted_country))
+ else:
+ self._country_combo.set_active(self._country_store.guess_country_row())
+
+ # Call the selected callback anyway, so as to chain-set the
+ # default values for providers and the plans.
+ self.__country_selected_cb(self._country_combo, setup=True)
+
+ self._model.get_modem_settings(self.populate_entries)
+
+ def populate_entries(self, settings):
self._populate_entry(self._username_entry,
settings.get('username', ''))
self._populate_entry(self._number_entry, settings.get('number', ''))
@@ -133,6 +211,78 @@ class ModemConfiguration(SectionView):
self._timeout_sid = GObject.timeout_add(APPLY_TIMEOUT,
self.__timeout_cb)
+ def _get_selected_text(self, combo):
+ active_iter = combo.get_active_iter()
+ return combo.get_model().get(active_iter, 0)[0]
+
+ def __country_selected_cb(self, combo, setup=False):
+ country = self._get_selected_text(combo)
+ self._model.set_gconf_setting_string(GSM_COUNTRY_PATH, country)
+
+ model = combo.get_model()
+ providers = model.get_row_providers(combo.get_active())
+ self._providers_liststore = self._model.ProviderListStore(providers)
+ self._providers_combo.set_model(self._providers_liststore)
+
+ # Set the default provider as well.
+ if setup:
+ persisted_provider = self._model.get_gconf_setting_string(GSM_PROVIDERS_PATH)
+ if persisted_provider == '':
+ self._providers_combo.set_active(self._providers_liststore.guess_providers_row())
+ else:
+ self._providers_combo.set_active(self._providers_liststore.search_index_by_code(persisted_provider))
+ else:
+ self._providers_combo.set_active(self._providers_liststore.guess_providers_row())
+
+ # Country-combobox processed once at startip; now, attach the
+ # change-handler.
+ self._country_combo.connect('changed', self.__country_selected_cb, False)
+
+ # Call the callback, so that default provider may be set.
+ self.__provider_selected_cb(self._providers_combo, setup)
+
+ def __provider_selected_cb(self, combo, setup=False):
+ provider = self._get_selected_text(combo)
+ self._model.set_gconf_setting_string(GSM_PROVIDERS_PATH, provider)
+
+ model = combo.get_model()
+ plans = model.get_row_plans(combo.get_active())
+ self._plan_liststore = self._model.PlanListStore(plans)
+ self._plan_combo.set_model(self._plan_liststore)
+
+ # Set the default plan as well.
+ if setup:
+ persisted_plan = self._model.get_gconf_setting_string(GSM_PLAN_PATH)
+ if persisted_plan == '':
+ self._plan_combo.set_active(self._plan_liststore.guess_plan_row())
+ else:
+ self._plan_combo.set_active(self._plan_liststore.search_index_by_code(persisted_plan))
+ else:
+ self._plan_combo.set_active(self._plan_liststore.guess_plan_row())
+
+ # Providers-combobox processed once at startip; now, attach the
+ # change-handler.
+ self._providers_combo.connect('changed', self.__provider_selected_cb, False)
+
+ # Call the callback, so that the default plan is set.
+ self.__plan_selected_cb(self._plan_combo, setup)
+
+ def __plan_selected_cb(self, combo, setup=False):
+ plan = self._get_selected_text(combo)
+ self._model.set_gconf_setting_string(GSM_PLAN_PATH, plan)
+
+ # Plan-combobox processed once at startip; now, attach the
+ # change-handler.
+ self._plan_combo.connect('changed', self.__plan_selected_cb, False)
+
+ model = combo.get_model()
+ plan = model.get_row_plan(combo.get_active())
+
+ self._populate_entry(self._username_entry, plan['username'])
+ self._populate_entry(self._password_entry, plan['password'])
+ self._populate_entry(self._apn_entry, plan['apn'])
+ self._populate_entry(self._number_entry, plan['number'])
+
def __timeout_cb(self):
self._timeout_sid = 0
settings = {}
diff --git a/extensions/cpsection/network/model.py b/extensions/cpsection/network/model.py
index ae9e64d..83c3cf1 100644
--- a/extensions/cpsection/network/model.py
+++ b/extensions/cpsection/network/model.py
@@ -18,6 +18,10 @@
import logging
import dbus
+import os
+import subprocess
+import logging
+
from gettext import gettext as _
from gi.repository import GConf
@@ -30,6 +34,8 @@ _NM_IFACE = 'org.freedesktop.NetworkManager'
KEYWORDS = ['network', 'jabber', 'radio', 'server']
+_logger = logging.getLogger('ControlPanel - Network')
+
class ReadError(Exception):
def __init__(self, value):
@@ -154,3 +160,14 @@ def set_publish_information(value):
client = GConf.Client.get_default()
client.set_bool('/desktop/sugar/collaboration/publish_gadget', value)
return 0
+
+
+def launch_nm_connection_editor():
+ environment = os.environ.copy()
+ environment['PATH'] = '%s:/usr/sbin' % (environment['PATH'], )
+
+ try:
+ subprocess.Popen(['-c', 'sudo nm-connection-editor --type=802-11-wireless'],
+ shell=True)
+ except:
+ _logger.exception('Error running nm-connection-editor')
diff --git a/extensions/cpsection/network/view.py b/extensions/cpsection/network/view.py
index 9b89375..e4332e4 100644
--- a/extensions/cpsection/network/view.py
+++ b/extensions/cpsection/network/view.py
@@ -17,6 +17,7 @@
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
+from gi.repository import GConf
from gettext import gettext as _
from sugar3.graphics import style
@@ -30,6 +31,9 @@ ICON = 'module-network'
TITLE = _('Network')
_APPLY_TIMEOUT = 3000
+EXPLICIT_REBOOT_MESSAGE = _('Please restart your computer for changes to take effect.')
+
+gconf_client = GConf.Client.get_default()
class Network(SectionView):
@@ -51,6 +55,7 @@ class Network(SectionView):
self._radio_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
self._jabber_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
+ self._nm_connection_editor_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING)
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
@@ -178,8 +183,54 @@ class Network(SectionView):
workspace.pack_start(box_mesh, False, True, 0)
box_mesh.show()
+ if gconf_client.get_bool('/desktop/sugar/extensions/network/show_nm_connection_editor') is True:
+ box_nm_connection_editor = self.add_nm_connection_editor_launcher(workspace)
+
self.setup()
+ def add_nm_connection_editor_launcher(self, workspace):
+ separator_nm_connection_editor = Gtk.HSeparator()
+ workspace.pack_start(separator_nm_connection_editor, False, True, 0)
+ separator_nm_connection_editor.show()
+
+ label_nm_connection_editor = Gtk.Label(_('Advanced Network Settings'))
+ label_nm_connection_editor.set_alignment(0, 0)
+ workspace.pack_start(label_nm_connection_editor, False, True, 0)
+ label_nm_connection_editor.show()
+
+ box_nm_connection_editor = Gtk.VBox()
+ box_nm_connection_editor.set_border_width(style.DEFAULT_SPACING * 2)
+ box_nm_connection_editor.set_spacing(style.DEFAULT_SPACING)
+
+ info = Gtk.Label(_("For more specific network settings, use "
+ "the NetworkManager Connection Editor."))
+
+ info.set_alignment(0, 0)
+ info.set_line_wrap(True)
+ box_nm_connection_editor.pack_start(info, False, True, 0)
+
+ self._nm_connection_editor_alert = InlineAlert()
+ self._nm_connection_editor_alert.props.msg = EXPLICIT_REBOOT_MESSAGE
+ self._nm_connection_editor_alert_box.pack_start(self._nm_connection_editor_alert,
+ False, True, 0)
+ box_nm_connection_editor.pack_end(self._nm_connection_editor_alert_box,
+ False, True, 0)
+ self._nm_connection_editor_alert_box.show()
+ self._nm_connection_editor_alert.show()
+
+ launch_button = Gtk.Button()
+ launch_button.set_alignment(0, 0)
+ launch_button.set_label(_('Launch'))
+ launch_button.connect('clicked', self.__launch_button_clicked_cb)
+ box_launch_button = Gtk.HBox()
+ box_launch_button.set_homogeneous(False)
+ box_launch_button.pack_start(launch_button, False, True, 0)
+ box_launch_button.show_all()
+
+ box_nm_connection_editor.pack_start(box_launch_button, False, True, 0)
+ workspace.pack_start(box_nm_connection_editor, False, True, 0)
+ box_nm_connection_editor.show_all()
+
def setup(self):
self._entry.set_text(self._model.get_jabber())
try:
@@ -260,3 +311,7 @@ class Network(SectionView):
self._model.clear_networks()
if not self._model.have_networks():
self._clear_history_button.set_sensitive(False)
+
+ def __launch_button_clicked_cb(self, launch_button):
+ self._model.launch_nm_connection_editor()
+