diff options
author | Simon Schampijer <simon@schampijer.de> | 2009-08-12 07:13:21 (GMT) |
---|---|---|
committer | Simon Schampijer <simon@schampijer.de> | 2009-08-12 07:13:21 (GMT) |
commit | c8016588eaefed5a55daf6e2387087a202cd5179 (patch) | |
tree | 5c723e24aa89c0c4ce6ebe54f6a93a7acf51ff8c /extensions | |
parent | f0d5242a11e5a6f5430629420c5e64d74734ac92 (diff) | |
parent | 0224153536d2fbdbd318211173171074f38251cb (diff) |
Merge branch 'master' of gitorious@git.sugarlabs.org:sugar/mainline
Diffstat (limited to 'extensions')
-rw-r--r-- | extensions/cpsection/Makefile.am | 2 | ||||
-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 | 157 | ||||
-rwxr-xr-x | extensions/cpsection/updater/model.py | 292 | ||||
-rw-r--r-- | extensions/cpsection/updater/view.py | 382 |
8 files changed, 883 insertions, 1 deletions
diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am index 73e5164..ffca6ca 100644 --- a/extensions/cpsection/Makefile.am +++ b/extensions/cpsection/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = aboutme aboutcomputer datetime frame language network power +SUBDIRS = aboutme aboutcomputer datetime frame language network power updater sugardir = $(pkgdatadir)/extensions/cpsection sugar_PYTHON = __init__.py 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..8c01ec2 --- /dev/null +++ b/extensions/cpsection/updater/backends/aslo.py @@ -0,0 +1,157 @@ +#!/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..489cfa5 --- /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) |