diff options
Diffstat (limited to 'extensions')
-rw-r--r-- | extensions/cpsection/Makefile.am | 2 | ||||
-rw-r--r-- | extensions/cpsection/aboutcomputer/model.py | 2 | ||||
-rw-r--r-- | extensions/cpsection/language/view.py | 10 | ||||
-rw-r--r-- | extensions/cpsection/network/view.py | 21 | ||||
-rw-r--r-- | extensions/cpsection/updater/Makefile.am | 8 | ||||
-rw-r--r-- | extensions/cpsection/updater/__init__.py | 22 | ||||
-rw-r--r-- | extensions/cpsection/updater/backends/Makefile.am | 5 | ||||
-rw-r--r-- | extensions/cpsection/updater/backends/__init__.py | 16 | ||||
-rw-r--r-- | extensions/cpsection/updater/backends/aslo.py | 156 | ||||
-rwxr-xr-x | extensions/cpsection/updater/model.py | 292 | ||||
-rw-r--r-- | extensions/cpsection/updater/view.py | 382 | ||||
-rw-r--r-- | extensions/deviceicon/battery.py | 8 | ||||
-rw-r--r-- | extensions/deviceicon/network.py | 50 | ||||
-rw-r--r-- | extensions/deviceicon/volume.py | 2 |
14 files changed, 950 insertions, 26 deletions
diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am index 4bee4ff..dd0a6b8 100644 --- a/extensions/cpsection/Makefile.am +++ b/extensions/cpsection/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = aboutme aboutcomputer datetime frame keyboard language network power +SUBDIRS = aboutme aboutcomputer datetime frame keyboard language network power updater sugardir = $(pkgdatadir)/extensions/cpsection sugar_PYTHON = __init__.py diff --git a/extensions/cpsection/aboutcomputer/model.py b/extensions/cpsection/aboutcomputer/model.py index 6cddb4c..898d79c 100644 --- a/extensions/cpsection/aboutcomputer/model.py +++ b/extensions/cpsection/aboutcomputer/model.py @@ -111,7 +111,7 @@ def _read_file(path): value = value.strip('\n') return value else: - _logger.debug('No information in file or directory: %s' % path) + _logger.debug('No information in file or directory: %s', path) return None def get_license(): diff --git a/extensions/cpsection/language/view.py b/extensions/cpsection/language/view.py index 30dc05d..a896aa5 100644 --- a/extensions/cpsection/language/view.py +++ b/extensions/cpsection/language/view.py @@ -1,4 +1,5 @@ # Copyright (C) 2008, OLPC +# Copyright (C) 2009, Simon Schampijer # # 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 @@ -52,6 +53,15 @@ class Language(SectionView): self.set_border_width(style.DEFAULT_SPACING * 2) self.set_spacing(style.DEFAULT_SPACING) + explanation = gettext.gettext("Add languages in the order you prefer." \ + " If a translation is not available,"\ + " the next in the list will be used.") + self._text = gtk.Label(explanation) + self._text.set_line_wrap(True) + self._text.set_alignment(0, 0) + self.pack_start(self._text, False) + self._text.show() + self._table = gtk.Table(rows=1, columns=3, homogeneous=False) self.pack_start(self._table, False) self._table.show() diff --git a/extensions/cpsection/network/view.py b/extensions/cpsection/network/view.py index ef28f00..588daeb 100644 --- a/extensions/cpsection/network/view.py +++ b/extensions/cpsection/network/view.py @@ -49,13 +49,16 @@ class Network(SectionView): self._radio_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) self._jabber_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + workspace = gtk.VBox() + workspace.show() + separator_wireless = gtk.HSeparator() - self.pack_start(separator_wireless, expand=False) + workspace.pack_start(separator_wireless, expand=False) separator_wireless.show() label_wireless = gtk.Label(_('Wireless')) label_wireless.set_alignment(0, 0) - self.pack_start(label_wireless, expand=False) + workspace.pack_start(label_wireless, expand=False) label_wireless.show() box_wireless = gtk.VBox() box_wireless.set_border_width(style.DEFAULT_SPACING * 2) @@ -105,16 +108,16 @@ class Network(SectionView): box_wireless.pack_start(box_clear_history, expand=False) box_clear_history.show() - self.pack_start(box_wireless, expand=False) + workspace.pack_start(box_wireless, expand=False) box_wireless.show() separator_mesh = gtk.HSeparator() - self.pack_start(separator_mesh, False) + workspace.pack_start(separator_mesh, False) separator_mesh.show() label_mesh = gtk.Label(_('Collaboration')) label_mesh.set_alignment(0, 0) - self.pack_start(label_mesh, expand=False) + workspace.pack_start(label_mesh, expand=False) label_mesh.show() box_mesh = gtk.VBox() box_mesh.set_border_width(style.DEFAULT_SPACING * 2) @@ -161,9 +164,15 @@ class Network(SectionView): self._jabber_alert.props.msg = self.restart_msg self._jabber_alert.show() - self.pack_start(box_mesh, expand=False) + workspace.pack_start(box_mesh, expand=False) box_mesh.show() + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrolled.add_with_viewport(workspace) + scrolled.show() + self.add(scrolled) + self.setup() def setup(self): diff --git a/extensions/cpsection/updater/Makefile.am b/extensions/cpsection/updater/Makefile.am new file mode 100644 index 0000000..897dbf3 --- /dev/null +++ b/extensions/cpsection/updater/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = backends + +sugardir = $(pkgdatadir)/extensions/cpsection/updater + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/extensions/cpsection/updater/__init__.py b/extensions/cpsection/updater/__init__.py new file mode 100644 index 0000000..6010615 --- /dev/null +++ b/extensions/cpsection/updater/__init__.py @@ -0,0 +1,22 @@ +# Copyright (C) 2008, OLPC +# +# 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 USA + +from gettext import gettext as _ + +CLASS = 'ActivityUpdater' +ICON = 'module-updater' +TITLE = _('Software update') +KEYWORDS = ['software', 'activity', 'update'] diff --git a/extensions/cpsection/updater/backends/Makefile.am b/extensions/cpsection/updater/backends/Makefile.am new file mode 100644 index 0000000..e280a07 --- /dev/null +++ b/extensions/cpsection/updater/backends/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pkgdatadir)/extensions/cpsection/updater/backends + +sugar_PYTHON = \ + aslo.py \ + __init__.py diff --git a/extensions/cpsection/updater/backends/__init__.py b/extensions/cpsection/updater/backends/__init__.py new file mode 100644 index 0000000..0dd0174 --- /dev/null +++ b/extensions/cpsection/updater/backends/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# Copyright (C) 2009, Sugar Labs +# +# 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 USA diff --git a/extensions/cpsection/updater/backends/aslo.py b/extensions/cpsection/updater/backends/aslo.py new file mode 100644 index 0000000..443fd1c --- /dev/null +++ b/extensions/cpsection/updater/backends/aslo.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# Copyright (C) 2009, Sugar Labs +# +# 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 USA + +'''Activity information microformat parser. + +Activity information is embedded in HTML/XHTML/XML pages using a +Resource Description Framework (RDF) http://www.w3.org/RDF/ . + +An example:: + +<?xml version="1.0" encoding="UTF-8"?> +<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> +<RDF:Description about="urn:mozilla:extension:bounce"> + <em:updates> + <RDF:Seq> + <RDF:li resource="urn:mozilla:extension:bounce:7"/> + </RDF:Seq> + </em:updates> +</RDF:Description> + +<RDF:Description about="urn:mozilla:extension:bounce:7"> + <em:version>7</em:version> + <em:targetApplication> + <RDF:Description> + <em:id>{3ca105e0-2280-4897-99a0-c277d1b733d2}</em:id> + <em:minVersion>0.82</em:minVersion> + <em:maxVersion>0.84</em:maxVersion> + <em:updateLink>http://foo.xo</em:updateLink> + <em:updateSize>7</em:updateSize> + <em:updateHash>sha256:816a7c43b4f1ea4769c61c03ea4..</em:updateHash> + </RDF:Description> + </em:targetApplication> +</RDF:Description></RDF:RDF> +''' + +import logging +from xml.etree.ElementTree import XML +import traceback + +import gio + +from jarabe import config + +_FIND_DESCRIPTION = \ + './/{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description' +_FIND_VERSION = './/{http://www.mozilla.org/2004/em-rdf#}version' +_FIND_LINK = './/{http://www.mozilla.org/2004/em-rdf#}updateLink' +_FIND_SIZE = './/{http://www.mozilla.org/2004/em-rdf#}updateSize' + +_UPDATE_PATH = 'http://activities.sugarlabs.org/services/update.php' + +_fetcher = None + + +class _UpdateFetcher(object): + + _CHUNK_SIZE = 10240 + + def __init__(self, bundle, completion_cb): + + url = '%s?id=%s&appVersion=%s' % \ + (_UPDATE_PATH, bundle.get_bundle_id(), config.version) + + self._completion_cb = completion_cb + self._file = gio.File(url) + self._stream = None + self._xml_data = '' + self._bundle = bundle + + self._file.read_async(self.__file_read_async_cb) + + def __file_read_async_cb(self, gfile, result): + try: + self._stream = self._file.read_finish(result) + except: + global _fetcher + _fetcher = None + self._completion_cb(None, None, None, None, traceback.format_exc()) + return + + self._stream.read_async(self._CHUNK_SIZE, self.__stream_read_async_cb) + + def __stream_read_async_cb(self, stream, result): + xml_data = self._stream.read_finish(result) + if xml_data is None: + global _fetcher + _fetcher = None + self._completion_cb(self._bundle, None, None, None, + 'Error reading update information for %s from ' + 'server.' % self._bundle.get_bundle_id()) + return + elif not xml_data: + self._process_result() + else: + self._xml_data += xml_data + self._stream.read_async(self._CHUNK_SIZE, + self.__stream_read_async_cb) + + def _process_result(self): + document = XML(self._xml_data) + + if document.find(_FIND_DESCRIPTION) is None: + logging.debug('Bundle %s not available in the server for the ' + 'version %s', self._bundle.get_bundle_id(), config.version) + version = None + link = None + size = None + else: + try: + version = int(document.find(_FIND_VERSION).text) + except ValueError: + logging.error(traceback.format_exc()) + version = 0 + + link = document.find(_FIND_LINK).text + + try: + size = long(document.find(_FIND_SIZE).text) * 1024 + except ValueError: + logging.error(traceback.format_exc()) + size = 0 + + global _fetcher + _fetcher = None + self._completion_cb(self._bundle, version, link, size, None) + + +def fetch_update_info(bundle, completion_cb): + '''Queries the server for a newer version of the ActivityBundle. + + completion_cb receives bundle, version, link, size and possibly an error + message: + + def completion_cb(bundle, version, link, size, error_message): + ''' + global _fetcher + + if _fetcher is not None: + raise RuntimeError('Multiple simultaneous requests are not supported') + + _fetcher = _UpdateFetcher(bundle, completion_cb) diff --git a/extensions/cpsection/updater/model.py b/extensions/cpsection/updater/model.py new file mode 100755 index 0000000..102edea --- /dev/null +++ b/extensions/cpsection/updater/model.py @@ -0,0 +1,292 @@ +# Copyright (C) 2009, Sugar Labs +# Copyright (C) 2009, Tomeu Vizoso +# +# 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 USA +'''Sugar bundle updater: model. + +This module implements the non-GUI portions of the bundle updater, including +list of installed bundls, whether updates are needed, and the URL at which to +find the bundle updated. +''' + +import os +import logging +import tempfile +from urlparse import urlparse +import traceback + +import gobject +import gio + +from sugar import env +from sugar.datastore import datastore +from sugar.bundle.activitybundle import ActivityBundle + +from jarabe.model import bundleregistry + +from backends import aslo + + +class UpdateModel(gobject.GObject): + __gtype_name__ = 'SugarUpdateModel' + + __gsignals__ = { + 'progress': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([int, str, float, int])), + } + + ACTION_CHECKING = 0 + ACTION_UPDATING = 1 + ACTION_DOWNLOADING = 2 + + def __init__(self): + gobject.GObject.__init__(self) + + self.updates = None + self._bundles_to_check = None + self._bundles_to_update = None + self._total_bundles_to_update = 0 + self._downloader = None + + def check_updates(self): + self.updates = [] + self._bundles_to_check = \ + [bundle for bundle in bundleregistry.get_registry()] + self._check_next_update() + + def _check_next_update(self): + total = len(bundleregistry.get_registry()) + current = total - len(self._bundles_to_check) + + bundle = self._bundles_to_check.pop() + self.emit('progress', UpdateModel.ACTION_CHECKING, bundle.get_name(), + current, total) + + aslo.fetch_update_info(bundle, self.__check_completed_cb) + + def __check_completed_cb(self, bundle, version, link, size, error_message): + if error_message is not None: + logging.error('Error getting update information from server:\n' + '%s' % error_message) + + if version is not None and version > bundle.get_activity_version(): + self.updates.append(BundleUpdate(bundle, version, link, size)) + + if self._bundles_to_check: + gobject.idle_add(self._check_next_update) + else: + total = len(bundleregistry.get_registry()) + if bundle is None: + name = '' + else: + name = bundle.get_name() + self.emit('progress', UpdateModel.ACTION_CHECKING, name, total, + total) + + def update(self, bundle_ids): + self._bundles_to_update = [] + for bundle_update in self.updates: + if bundle_update.bundle.get_bundle_id() in bundle_ids: + self._bundles_to_update.append(bundle_update) + + self._total_bundles_to_update = len(self._bundles_to_update) + self._download_next_update() + + def _download_next_update(self): + bundle_update = self._bundles_to_update.pop() + + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 - 2 + + self.emit('progress', UpdateModel.ACTION_DOWNLOADING, + bundle_update.bundle.get_name(), current, total) + + self._downloader = _Downloader(bundle_update) + self._downloader.connect('progress', self.__downloader_progress_cb) + self._downloader.connect('error', self.__downloader_error_cb) + + def __downloader_progress_cb(self, downloader, progress): + logging.debug('__downloader_progress_cb %r', progress) + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 - 2 + progress + + self.emit('progress', UpdateModel.ACTION_DOWNLOADING, + self._downloader.bundle_update.bundle.get_name(), + current, total) + + if progress == 1: + self._install_update(self._downloader.bundle_update, + self._downloader.get_local_file_path()) + self._downloader = None + + def __downloader_error_cb(self, downloader, error_message): + logging.error('Error downloading update:\n%s', error_message) + + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 + self.emit('progress', UpdateModel.ACTION_UPDATING, '', current, total) + + if self._bundles_to_update: + # do it in idle so the UI has a chance to refresh + gobject.idle_add(self._download_next_update) + + def _install_update(self, bundle_update, local_file_path): + + total = self._total_bundles_to_update * 2 + current = total - len(self._bundles_to_update) * 2 - 1 + + self.emit('progress', UpdateModel.ACTION_UPDATING, + bundle_update.bundle.get_name(), + current, total) + + # TODO: Should we first expand the zip async so we can provide progress + # and only then copy to the journal? + jobject = datastore.create() + try: + title = '%s-%s' % (bundle_update.bundle.get_name(), + bundle_update.version) + jobject.metadata['title'] = title + jobject.metadata['mime_type'] = ActivityBundle.MIME_TYPE + jobject.file_path = local_file_path + datastore.write(jobject, transfer_ownership=True) + finally: + jobject.destroy() + + current += 1 + self.emit('progress', UpdateModel.ACTION_UPDATING, + bundle_update.bundle.get_name(), + current, total) + + if self._bundles_to_update: + # do it in idle so the UI has a chance to refresh + gobject.idle_add(self._download_next_update) + + def get_total_bundles_to_update(self): + return self._total_bundles_to_update + + +class BundleUpdate(object): + + def __init__(self, bundle, version, link, size): + self.bundle = bundle + self.version = version + self.link = link + self.size = size + + +class _Downloader(gobject.GObject): + _CHUNK_SIZE = 10240 # 10K + __gsignals__ = { + 'progress': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([float])), + 'error': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self, bundle_update): + gobject.GObject.__init__(self) + + self.bundle_update = bundle_update + self._input_stream = None + self._output_stream = None + self._pending_buffers = [] + self._input_file = gio.File(bundle_update.link) + self._output_file = None + self._downloaded_size = 0 + + self._input_file.read_async(self.__file_read_async_cb) + + def __file_read_async_cb(self, gfile, result): + try: + self._input_stream = self._input_file.read_finish(result) + except: + self.emit('error', traceback.format_exc()) + return + + temp_file_path = self._get_temp_file_path(self.bundle_update.link) + self._output_file = gio.File(temp_file_path) + self._output_stream = self._output_file.create() + + self._input_stream.read_async(self._CHUNK_SIZE, self.__read_async_cb, + gobject.PRIORITY_LOW) + + def __read_async_cb(self, input_stream, result): + data = input_stream.read_finish(result) + + if data is None: + # TODO + pass + elif not data: + logging.debug('closing input stream') + self._input_stream.close() + self._check_if_finished_writing() + else: + self._pending_buffers.append(data) + self._input_stream.read_async(self._CHUNK_SIZE, + self.__read_async_cb, + gobject.PRIORITY_LOW) + + self._write_next_buffer() + + def __write_async_cb(self, output_stream, result, user_data): + count = output_stream.write_finish(result) + + self._downloaded_size += count + progress = self._downloaded_size / float(self.bundle_update.size) + self.emit('progress', progress) + + self._check_if_finished_writing() + + if self._pending_buffers: + self._write_next_buffer() + + def _write_next_buffer(self): + if self._pending_buffers and not self._output_stream.has_pending(): + data = self._pending_buffers.pop(0) + # TODO: we pass the buffer as user_data because of + # http://bugzilla.gnome.org/show_bug.cgi?id=564102 + self._output_stream.write_async(data, self.__write_async_cb, + gobject.PRIORITY_LOW, + user_data=data) + + def _get_temp_file_path(self, uri): + # TODO: Should we use the HTTP headers for the file name? + scheme_, netloc_, path, params_, query_, fragment_ = \ + urlparse(uri) + path = os.path.basename(path) + + base_name, extension_ = os.path.splitext(path) + fd, file_path = tempfile.mkstemp(dir=env.get_user_activities_path(), + prefix=base_name, suffix='.xo') + os.close(fd) + os.unlink(file_path) + + return file_path + + def get_local_file_path(self): + return self._output_file.get_path() + + def _check_if_finished_writing(self): + if not self._pending_buffers and \ + not self._output_stream.has_pending() and \ + self._input_stream.is_closed(): + + logging.debug('closing output stream') + self._output_stream.close() + + self.emit('progress', 1.0) diff --git a/extensions/cpsection/updater/view.py b/extensions/cpsection/updater/view.py new file mode 100644 index 0000000..9a77743 --- /dev/null +++ b/extensions/cpsection/updater/view.py @@ -0,0 +1,382 @@ +# Copyright (C) 2008, One Laptop Per Child +# Copyright (C) 2009, Tomeu Vizoso +# +# 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 USA + +from gettext import gettext as _ +from gettext import ngettext +import locale + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics.icon import Icon, CellRendererIcon + +from jarabe.controlpanel.sectionview import SectionView + +from model import UpdateModel + +_DEBUG_VIEW_ALL = True + + +class ActivityUpdater(SectionView): + + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = UpdateModel() + self._model.connect('progress', self.__progress_cb) + + self.set_spacing(style.DEFAULT_SPACING) + self.set_border_width(style.DEFAULT_SPACING * 2) + + self._top_label = gtk.Label() + self._top_label.set_line_wrap(True) + self._top_label.set_justify(gtk.JUSTIFY_LEFT) + self._top_label.props.xalign = 0 + self.pack_start(self._top_label, expand=False) + self._top_label.show() + + separator = gtk.HSeparator() + self.pack_start(separator, expand=False) + separator.show() + + bottom_label = gtk.Label() + bottom_label.set_line_wrap(True) + bottom_label.set_justify(gtk.JUSTIFY_LEFT) + bottom_label.props.xalign = 0 + bottom_label.set_markup( + _('Software updates correct errors, eliminate security ' \ + 'vulnerabilities, and provide new features.')) + self.pack_start(bottom_label, expand=False) + bottom_label.show() + + self._update_box = None + self._progress_pane = None + + self._refresh() + + def _switch_to_update_box(self): + if self._update_box in self.get_children(): + return + + if self._progress_pane in self.get_children(): + self.remove(self._progress_pane) + self._progress_pane = None + + if self._update_box is None: + self._update_box = UpdateBox(self._model) + self._update_box.refresh_button.connect('clicked', + self.__refresh_button_clicked_cb) + self._update_box.install_button.connect('clicked', + self.__install_button_clicked_cb) + + self.pack_start(self._update_box, expand=True, fill=True) + self._update_box.show() + + def _switch_to_progress_pane(self): + if self._progress_pane in self.get_children(): + return + + if self._update_box in self.get_children(): + self.remove(self._update_box) + self._update_box = None + + if self._progress_pane is None: + self._progress_pane = ProgressPane() + + self.pack_start(self._progress_pane, expand=True, fill=False) + self._progress_pane.show() + + def _clear_center(self): + if self._progress_pane in self.get_children(): + self.remove(self._progress_pane) + self._progress_pane = None + + if self._update_box in self.get_children(): + self.remove(self._update_box) + self._update_box = None + + def __progress_cb(self, model, action, bundle_name, current, total): + if current == total and action == UpdateModel.ACTION_CHECKING: + self._finished_checking() + return + elif current == total: + self._finished_updating() + return + + if action == UpdateModel.ACTION_CHECKING: + message = _('Checking %s...') % bundle_name + elif action == UpdateModel.ACTION_DOWNLOADING: + message = _('Downloading %s...') % bundle_name + elif action == UpdateModel.ACTION_UPDATING: + message = _('Updating %s...') % bundle_name + + self._switch_to_progress_pane() + self._progress_pane.set_message(message) + self._progress_pane.set_progress(current / float(total)) + + def _finished_checking(self): + available_updates = len(self._model.updates) + if not available_updates: + top_message = _('Your software is up-to-date') + else: + top_message = ngettext('You can install %s update', + 'You can install %s updates', + available_updates) + top_message = top_message % available_updates + top_message = gobject.markup_escape_text(top_message) + + self._top_label.set_markup('<big>%s</big>' % top_message) + + if not available_updates: + self._clear_center() + else: + self._switch_to_update_box() + self._update_box.refresh() + + def __refresh_button_clicked_cb(self, button): + self._refresh() + + def _refresh(self): + top_message = _('Checking for updates...') + self._top_label.set_markup('<big>%s</big>' % top_message) + self._model.check_updates() + + def __install_button_clicked_cb(self, button): + self._top_label.set_markup('<big>%s</big>' % _('Installing updates...')) + self._model.update(self._update_box.get_bundles_to_update()) + + def _finished_updating(self): + installed_updates = self._model.get_total_bundles_to_update() + top_message = ngettext('%s update was installed', + '%s updates were installed', installed_updates) + top_message = top_message % installed_updates + top_message = gobject.markup_escape_text(top_message) + self._top_label.set_markup('<big>%s</big>' % top_message) + self._clear_center() + + +class ProgressPane(gtk.VBox): + '''Container which replaces the `ActivityPane` during refresh or + install.''' + + def __init__(self): + gtk.VBox.__init__(self) + self.set_spacing(style.DEFAULT_PADDING) + self.set_border_width(style.DEFAULT_SPACING * 2) + + self._progress = gtk.ProgressBar() + self.pack_start(self._progress) + self._progress.show() + + self._label = gtk.Label() + self._label.set_line_wrap(True) + self._label.set_property('xalign', 0.5) + self._label.modify_fg(gtk.STATE_NORMAL, + style.COLOR_BUTTON_GREY.get_gdk_color()) + self.pack_start(self._label) + self._label.show() + + alignment_box = gtk.Alignment(xalign=0.5, yalign=0.5) + self.pack_start(alignment_box) + alignment_box.show() + + cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL) + alignment_box.add(cancel_button) + cancel_button.show() + + def set_message(self, message): + self._label.set_text(message) + + def set_progress(self, fraction): + self._progress.props.fraction = fraction + + +class UpdateBox(gtk.VBox): + + def __init__(self, model): + gtk.VBox.__init__(self) + + self._model = model + + self.set_spacing(style.DEFAULT_PADDING) + + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.pack_start(scrolled_window) + scrolled_window.show() + + self._update_list = UpdateList(model) + self._update_list.props.model.connect('row-changed', + self.__row_changed_cb) + scrolled_window.add(self._update_list) + self._update_list.show() + + bottom_box = gtk.HBox() + bottom_box.set_spacing(style.DEFAULT_SPACING) + self.pack_start(bottom_box, expand=False) + bottom_box.show() + + self._size_label = gtk.Label() + self._size_label.props.xalign = 0 + self._size_label.set_justify(gtk.JUSTIFY_LEFT) + bottom_box.pack_start(self._size_label, expand=True) + self._size_label.show() + + self.refresh_button = gtk.Button(stock=gtk.STOCK_REFRESH) + bottom_box.pack_start(self.refresh_button, expand=False) + self.refresh_button.show() + + self.install_button = gtk.Button(_('Install selected')) + self.install_button.props.image = Icon(icon_name='emblem-downloads', + icon_size=gtk.ICON_SIZE_BUTTON) + bottom_box.pack_start(self.install_button, expand=False) + self.install_button.show() + + self._update_total_size_label() + + def refresh(self): + self._update_list.refresh() + + def __row_changed_cb(self, list_model, path, iterator): + self._update_total_size_label() + self._update_install_button() + + def _update_total_size_label(self): + total_size = 0 + for row in self._update_list.props.model: + if row[UpdateListModel.SELECTED]: + total_size += row[UpdateListModel.SIZE] + + markup = _('Download size: %s') % _format_size(total_size) + self._size_label.set_markup(markup) + + def _update_install_button(self): + for row in self._update_list.props.model: + if row[UpdateListModel.SELECTED]: + self.install_button.props.sensitive = True + return + self.install_button.props.sensitive = False + + def get_bundles_to_update(self): + bundles_to_update = [] + for row in self._update_list.props.model: + if row[UpdateListModel.SELECTED]: + bundles_to_update.append(row[UpdateListModel.BUNDLE_ID]) + return bundles_to_update + + +class UpdateList(gtk.TreeView): + + def __init__(self, model): + list_model = UpdateListModel(model) + gtk.TreeView.__init__(self, list_model) + + self.set_reorderable(False) + self.set_enable_search(False) + self.set_headers_visible(False) + + toggle_renderer = gtk.CellRendererToggle() + toggle_renderer.props.activatable = True + toggle_renderer.props.xpad = style.DEFAULT_PADDING + toggle_renderer.props.indicator_size = style.zoom(26) + toggle_renderer.connect('toggled', self.__toggled_cb) + + toggle_column = gtk.TreeViewColumn() + toggle_column.pack_start(toggle_renderer) + toggle_column.add_attribute(toggle_renderer, 'active', + UpdateListModel.SELECTED) + self.append_column(toggle_column) + + icon_renderer = CellRendererIcon(self) + icon_renderer.props.width = style.STANDARD_ICON_SIZE + icon_renderer.props.height = style.STANDARD_ICON_SIZE + icon_renderer.props.size = style.STANDARD_ICON_SIZE + icon_renderer.props.xpad = style.DEFAULT_PADDING + icon_renderer.props.ypad = style.DEFAULT_PADDING + icon_renderer.props.stroke_color = style.COLOR_TOOLBAR_GREY.get_svg() + icon_renderer.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + + icon_column = gtk.TreeViewColumn() + icon_column.pack_start(icon_renderer) + icon_column.add_attribute(icon_renderer, 'file-name', + UpdateListModel.ICON_FILE_NAME) + self.append_column(icon_column) + + text_renderer = gtk.CellRendererText() + + description_column = gtk.TreeViewColumn() + description_column.pack_start(text_renderer) + description_column.add_attribute(text_renderer, 'markup', + UpdateListModel.DESCRIPTION) + self.append_column(description_column) + + def __toggled_cb(self, cell_renderer, path): + row = self.props.model[path] + row[UpdateListModel.SELECTED] = not row[UpdateListModel.SELECTED] + + def refresh(self): + pass + + +class UpdateListModel(gtk.ListStore): + + BUNDLE_ID = 0 + SELECTED = 1 + ICON_FILE_NAME = 2 + DESCRIPTION = 3 + SIZE = 4 + + def __init__(self, model): + gtk.ListStore.__init__(self, str, bool, str, str, int) + + for bundle_update in model.updates: + row = [None] * 5 + row[self.BUNDLE_ID] = bundle_update.bundle.get_bundle_id() + row[self.SELECTED] = True + row[self.ICON_FILE_NAME] = bundle_update.bundle.get_icon() + + details = _('From version %(current)d to %(new)s (Size: %(size)s)') + details = details % \ + {'current': bundle_update.bundle.get_activity_version(), + 'new': bundle_update.version, + 'size': _format_size(bundle_update.size)} + + row[self.DESCRIPTION] = '<b>%s</b>\n%s' % \ + (bundle_update.bundle.get_name(), details) + + row[self.SIZE] = bundle_update.size + + self.append(row) + + +def _format_size(size): + ''' + Convert a given size in bytes to a nicer better readable unit + ''' + if size == 0: + # TRANS: download size is 0 + return _('None') + elif size < 1024: + # TRANS: download size of very small updates + return _('1 KB') + elif size < 1024 * 1024: + # TRANS: download size of small updates, e.g. '250 KB' + return locale.format(_('%.0f KB'), size / 1024.0) + else: + # TRANS: download size of updates, e.g. '2.3 MB' + return locale.format(_('%.1f MB'), size / 1024.0 / 1024) diff --git a/extensions/deviceicon/battery.py b/extensions/deviceicon/battery.py index 76b1565..edfcce4 100644 --- a/extensions/deviceicon/battery.py +++ b/extensions/deviceicon/battery.py @@ -189,28 +189,28 @@ class DeviceModel(gobject.GObject): try: return self._battery.GetProperty(_LEVEL_PROP) except dbus.DBusException: - logging.error('Cannot access %s' % _LEVEL_PROP) + logging.error('Cannot access %s', _LEVEL_PROP) return 0 def _get_charging(self): try: return self._battery.GetProperty(_CHARGING_PROP) except dbus.DBusException: - logging.error('Cannot access %s' % _CHARGING_PROP) + logging.error('Cannot access %s', _CHARGING_PROP) return False def _get_discharging(self): try: return self._battery.GetProperty(_DISCHARGING_PROP) except dbus.DBusException: - logging.error('Cannot access %s' % _DISCHARGING_PROP) + logging.error('Cannot access %s', _DISCHARGING_PROP) return False def _get_present(self): try: return self._battery.GetProperty(_PRESENT_PROP) except dbus.DBusException: - logging.error('Cannot access %s' % _PRESENT_PROP) + logging.error('Cannot access %s', _PRESENT_PROP) return False def do_get_property(self, pspec): diff --git a/extensions/deviceicon/network.py b/extensions/deviceicon/network.py index 0abf25e..0803936 100644 --- a/extensions/deviceicon/network.py +++ b/extensions/deviceicon/network.py @@ -1,5 +1,6 @@ # # Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer # # 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 @@ -17,7 +18,7 @@ from gettext import gettext as _ import logging -import sha +import hashlib import socket import struct @@ -124,6 +125,12 @@ class WirelessPalette(Palette): self._set_channel(frequency) self._set_ip_address(iaddress) self._disconnect_item.show() + + def set_disconnected(self): + self.props.primary_text = '' + self.props.secondary_text = '' + self._disconnect_item.hide() + self.set_content(None) def __disconnect_activate_cb(self, menuitem): self.emit('deactivate-connection') @@ -208,6 +215,7 @@ class WirelessDeviceView(ToolButton): self._device_props = None self._flags = 0 self._name = '' + self._mode = network.NM_802_11_MODE_UNKNOWN self._strength = 0 self._frequency = 0 self._device_state = None @@ -301,8 +309,12 @@ class WirelessDeviceView(ToolButton): self._update_properties(properties) def _update_properties(self, properties): + if 'Mode' in properties: + self._mode = properties['Mode'] + self._color = None if 'Ssid' in properties: self._name = properties['Ssid'] + self._color = None if 'Strength' in properties: self._strength = properties['Strength'] if 'Flags' in properties: @@ -310,14 +322,21 @@ class WirelessDeviceView(ToolButton): if 'Frequency' in properties: self._frequency = properties['Frequency'] - sh = sha.new() - data = self._name + hex(self._flags) - sh.update(data) - h = hash(sh.digest()) - idx = h % len(xocolor.colors) - - self._color = xocolor.XoColor('%s,%s' % (xocolor.colors[idx][0], - xocolor.colors[idx][1])) + if self._color == None: + if self._mode == network.NM_802_11_MODE_ADHOC: + encoded_color = self._name.split("#", 1) + if len(encoded_color) == 2: + self._color = xocolor.XoColor('#' + encoded_color[1]) + if self._mode == network.NM_802_11_MODE_INFRA: + sha_hash = hashlib.sha1() + data = self._name + hex(self._flags) + sha_hash.update(data) + digest = hash(sha_hash.digest()) + index = digest % len(xocolor.colors) + + self._color = xocolor.XoColor('%s,%s' % + (xocolor.colors[index][0], + xocolor.colors[index][1])) self._update() def __get_all_ap_props_reply_cb(self, properties): @@ -362,6 +381,12 @@ class WirelessDeviceView(ToolButton): address = self._device_props.Get(_NM_DEVICE_IFACE, 'Ip4Address') self._palette.set_connected(self._frequency, address) self._icon.props.pulsing = False + else: + self._icon.props.badge_name = None + self._icon.props.pulsing = False + self._icon.props.pulse_color = self._inactive_color + self._icon.props.base_color = self._inactive_color + self._palette.set_disconnected() def _update_color(self): self._icon.props.base_color = self._color @@ -386,7 +411,8 @@ class WirelessDeviceView(ToolButton): def __create_connection_cb(self, palette, data=None): client = gconf.client_get_default() nick = client.get_string('/desktop/sugar/user/nick') - connection_name = _('%s\'s network') % nick + color = client.get_string('/desktop/sugar/user/color') + connection_name = _('%s\'s network %s') % (nick, color) connection = network.find_connection(connection_name) if connection is None: @@ -394,11 +420,9 @@ class WirelessDeviceView(ToolButton): settings.connection.id = 'Auto ' + connection_name settings.connection.uuid = unique_id() settings.connection.type = '802-11-wireless' - settings.connection.mode = 'adhoc' settings.wireless.ssid = dbus.ByteArray(connection_name) - settings.wireless.channel = 'bg' + settings.wireless.band = 'bg' settings.wireless.mode = 'adhoc' - settings.ip4_config = IP4Config() settings.ip4_config.method = 'shared' diff --git a/extensions/deviceicon/volume.py b/extensions/deviceicon/volume.py index 5c4c49e..4ccae7a 100644 --- a/extensions/deviceicon/volume.py +++ b/extensions/deviceicon/volume.py @@ -97,7 +97,7 @@ def _mount(volume, tray): volume.mount(gtk.MountOperation(tray.get_toplevel()), _mount_cb) def _mount_cb(volume, result): - logging.debug('_mount_cb %r %r' % (volume, result)) + logging.debug('_mount_cb %r %r', volume, result) volume.mount_finish(result) def _mount_added_cb(volume_monitor, mount, tray): |