Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/extensions
diff options
context:
space:
mode:
authorTomeu Vizoso <tomeu@sugarlabs.org>2009-08-06 11:22:14 (GMT)
committer Tomeu Vizoso <tomeu@sugarlabs.org>2009-08-06 11:22:14 (GMT)
commit7f90679c8453e3aefc42f8bf03b00694864db55f (patch)
tree0fc686e9e346968980b9d38dd63240a1f26c7e79 /extensions
parentf79f3e31a7bb6a449911bbaec4df0672fcc01bf3 (diff)
Rewrite activity updater for:
- Drop unused functionality - Not use threads - Improve the class design - Use the same coding style as the rest of Sugar
Diffstat (limited to 'extensions')
-rw-r--r--extensions/cpsection/updater/backends/aslo.py133
-rwxr-xr-xextensions/cpsection/updater/model.py472
-rw-r--r--extensions/cpsection/updater/view.py652
3 files changed, 660 insertions, 597 deletions
diff --git a/extensions/cpsection/updater/backends/aslo.py b/extensions/cpsection/updater/backends/aslo.py
index be76b4c..8c01ec2 100644
--- a/extensions/cpsection/updater/backends/aslo.py
+++ b/extensions/cpsection/updater/backends/aslo.py
@@ -15,7 +15,7 @@
# 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 microformat parser.
Activity information is embedded in HTML/XHTML/XML pages using a
Resource Description Framework (RDF) http://www.w3.org/RDF/ .
@@ -46,13 +46,14 @@ An example::
</RDF:Description>
</em:targetApplication>
</RDF:Description></RDF:RDF>
-"""
+'''
import logging
-import urllib2
from xml.etree.ElementTree import XML
import traceback
+import gio
+
from jarabe import config
_FIND_DESCRIPTION = \
@@ -63,38 +64,94 @@ _FIND_SIZE = './/{http://www.mozilla.org/2004/em-rdf#}updateSize'
_UPDATE_PATH = 'http://activities.sugarlabs.org/services/update.php'
-def _parse_url(url, bundle_id):
- response = urllib2.urlopen(url)
- document = XML(response.read())
-
- if document.find(_FIND_DESCRIPTION) is None:
- logging.debug('Bundle %s not available in the server for the version'
- ' %s' % (bundle_id, config.version))
- return '0', None, 0
-
- version = document.find(_FIND_VERSION).text
- link = document.find(_FIND_LINK).text
- size = long(document.find(_FIND_SIZE).text) * 1024
-
- return version, link, size
-
-def fetch_update_info(bundle):
- """Return a tuple of (version, link, size_in_bytes) for new version.
-
- Returns ('0', None, 0) if no update is found for the platform version.
- """
-
- url = '%s?id=%s&appVersion=%s' % \
- (_UPDATE_PATH, bundle.get_bundle_id(), '0.84.1') #config.version)
-
- return _parse_url(url, bundle.get_bundle_id())
-
-#########################################################################
-# Self-test code.
-def _main():
- """Self-test."""
- print _parse_url('%s?id=bounce' % _UPDATE_PATH, 'bounce')
-
-if __name__ == '__main__':
- _main()
-
+_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
index 7a4a908..489cfa5 100755
--- a/extensions/cpsection/updater/model.py
+++ b/extensions/cpsection/updater/model.py
@@ -1,5 +1,5 @@
-#!/usr/bin/python
# 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
@@ -14,263 +14,279 @@
# 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.
+'''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.
-
-`UpdateList` inherits from `gtk.ListStore` in order to work closely with the
-view pane. This module requires `gtk`.
-"""
+'''
import os
-import tempfile
-import locale
import logging
-import urllib
-from gettext import gettext as _
+import tempfile
+from urlparse import urlparse
import traceback
import gobject
-import gtk
+import gio
-from sugar.bundle.activitybundle import ActivityBundle
+from sugar import env
from sugar.datastore import datastore
+from sugar.bundle.activitybundle import ActivityBundle
from jarabe.model import bundleregistry
+
from backends import aslo
-"""List of columns in the `UpdateList`."""
-BUNDLE_ID, \
- BUNDLE, \
- ICON, \
- NAME, \
- CURRENT_VERSION, \
- UPDATE_VERSION, \
- UPDATE_SIZE, \
- UPDATE_URL, \
- DESCRIPTION, \
- UPDATE_SELECTED, \
- UPDATE_AVAILABLE, \
- IS_HEADER = xrange(12)
-
-class UpdateList(gtk.ListStore):
- """Model which provides backing storage for the BUNDLE list treeview."""
-
- __gproperties__ = {
- 'is_valid': (gobject.TYPE_BOOLEAN, 'is valid',
- 'true iff the UpdateList has been properly refreshed',
- False, gobject.PARAM_READABLE),
+
+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):
- logging.debug('STARTUP: Loading the bundle updater')
+ 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()
- gtk.ListStore.__init__(self,
- str, object, gtk.gdk.Pixbuf, str,
- long, long, long, str,
- str, bool, bool, bool)
+ total = self._total_bundles_to_update * 2
+ current = total - len(self._bundles_to_update) * 2 - 2
- self._cancel = False
- self._is_valid = True
- self._registry = bundleregistry.get_registry()
- self._steps_count = 0
- self._steps_total = 0
- self._progress_cb = None
+ self.emit('progress', UpdateModel.ACTION_DOWNLOADING,
+ bundle_update.bundle.get_name(), current, total)
- def refresh_list(self, progress_callback=lambda n, extra: None,
- clear_cache=True):
- self._cancel = False
- self._progress_cb = progress_callback
- self._progress_cb(None, _('Looking for local actvities...'))
+ self._downloader = _Downloader(bundle_update)
+ self._downloader.connect('progress', self.__downloader_progress_cb)
+ self._downloader.connect('error', self.__downloader_error_cb)
- self.clear()
- self._steps_total = len([i for i in self._registry])
- self._steps_count = 0
+ 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
- row_map = {}
+ self.emit('progress', UpdateModel.ACTION_DOWNLOADING,
+ self._downloader.bundle_update.bundle.get_name(),
+ current, total)
- for bundle in self._registry:
- self._make_progress(_('Checking %s...') % bundle.get_name())
+ if progress == 1:
+ self._install_update(self._downloader.bundle_update,
+ self._downloader.get_local_file_path())
+ self._downloader = None
- if self._cancel:
- break # Cancel bundle refresh
+ def __downloader_error_cb(self, downloader, error_message):
+ logging.error('Error downloading update:\n%s' % error_message)
- row = [None, None, None, None,
- 0, 0, 0, None,
- None, True, False, False]
- row[BUNDLE] = bundle
- row[BUNDLE_ID] = bundle.get_bundle_id()
+ 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._refresh_row(row):
- row_map[row[BUNDLE_ID]] = self.get_path(self.append(row))
+ 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 cancel(self):
- self._cancel = True
+ def _install_update(self, bundle_update, local_file_path):
- def _refresh_row(self, row):
- logging.debug('Looking for %s' % row[BUNDLE].get_name())
+ 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:
- new_ver, new_url, new_size = aslo.fetch_update_info(row[BUNDLE])
- except Exception:
- logging.warning('Failure updating %s\n%s' % \
- (row[BUNDLE].get_name(), traceback.format_exc()))
- return False
-
- row[CURRENT_VERSION] = row[BUNDLE].get_activity_version()
- row[UPDATE_VERSION] = long(new_ver)
-
- if row[CURRENT_VERSION] >= row[UPDATE_VERSION]:
- logging.debug('Skip %s update' % row[BUNDLE].get_name())
- return False
-
- row[ICON] = gtk.gdk.pixbuf_new_from_file_at_size(
- row[BUNDLE].get_icon(), 32, 32)
- row[NAME] = row[BUNDLE].get_name()
- row[UPDATE_URL] = new_url
- row[UPDATE_AVAILABLE] = True
- row[UPDATE_SIZE] = new_size
- row[DESCRIPTION] = \
- _('From version %(current)d to %(new)s (Size: %(size)s)') % \
- {'current': row[CURRENT_VERSION],
- 'new': row[UPDATE_VERSION],
- 'size': _humanize_size(row[UPDATE_SIZE])}
- row[UPDATE_SELECTED] = True
-
- return True
-
- def install_updates(self, progress_cb=(lambda n, row: None)):
- self._cancel = False
- self._progress_cb = progress_cb
- self._steps_total = len([0 for row in self if row[UPDATE_SELECTED]]) * 2
- self._steps_count = 0
-
- installed = 0
-
- for row in self:
- if self._cancel:
- return installed
- if row[IS_HEADER]:
- continue
- if not row[UPDATE_SELECTED]:
- continue
-
- logging.debug('Downloading %s from %s' % \
- (row[NAME], row[UPDATE_URL]))
- self._make_progress(_('Downloading %s...') % row[NAME])
-
- fd, xofile = tempfile.mkstemp(suffix='.xo')
- os.close(fd)
- try:
- try:
- urllib.urlretrieve(row[UPDATE_URL], xofile)
- except Exception, e:
- logging.warning("Can't download %s: %s" % \
- (row[UPDATE_URL], e))
- continue
-
- if self._cancel:
- return installed
-
- logging.debug('Installing %s' % row[NAME])
- self._make_progress(_('Installing %s...') % row[NAME])
-
- jobject = datastore.create()
- jobject.metadata['title'] = \
- '%s-%s' % (row[NAME], row[UPDATE_VERSION])
- jobject.metadata['mime_type'] = ActivityBundle.MIME_TYPE
- jobject.file_path = xofile
- datastore.write(jobject, transfer_ownership=True)
-
- installed += 1
- finally:
- if os.path.exists(xofile):
- os.unlink(xofile)
-
- return installed
-
-###############################################################################
-
- def _make_progress(self, msg=None): #FIXME needs better name
- """Helper function to do progress update."""
- self._steps_count += 1
- self._progress_cb(float(self._steps_count)/self._steps_total, msg)
-
- def _sum_rows(self, row_func):
- """Sum the values returned by row_func called on all non-header
- rows."""
- return sum(row_func(r) for r in self if not r[IS_HEADER])
-
-###############################################################################
-
- def updates_available(self):
- """Return the number of updates available.
-
- Updated by `refresh`."""
- return self._sum_rows(lambda r: 1 if r[UPDATE_AVAILABLE] else 0)
-
- def updates_selected(self):
- """Return the number of updates selected."""
- return self._sum_rows(lambda r:
- 1 if r[UPDATE_AVAILABLE] and r[UPDATE_SELECTED] else 0)
-
- def updates_size(self):
- """Returns the size (in bytes) of the selected updates available.
-
- Updated by `refresh`."""
- return self._sum_rows(lambda r:
- r[UPDATE_SIZE] if r[UPDATE_AVAILABLE] and \
- r[UPDATE_SELECTED] else 0)
- def is_valid(self):
- """The UpdateList is invalidated before it is refreshed, and when
- the group information is modified without refreshing."""
- return self._is_valid
-
-###############################################################################
-# Utility Funtions
-
-def _humanize_size(bytes_):
- """
- Convert a given size in bytes to a nicer better readable unit
- """
- if bytes_ == 0:
- # TRANS: download size is 0
- return _('None')
- elif bytes_ < 1024:
- # TRANS: download size of very small updates
- return _('1 KB')
- elif bytes_ < 1024 * 1024:
- # TRANS: download size of small updates, e.g. '250 KB'
- return locale.format(_('%.0f KB'), bytes_ / 1024)
- else:
- # TRANS: download size of updates, e.g. '2.3 MB'
- return locale.format(_('%.1f MB'), bytes_ / 1024 / 1024)
-
-def print_available(ul):#FIXME this should onlu return available updates
- def opt(x):
- if x is None or x == '':
- return ''
- return ': %s' % x
- for row in ul:
- if row[IS_HEADER]:
- print row[NAME] + opt(row[DESCRIPTION])
+ 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:
- print '*', row[NAME] + opt(row[DESCRIPTION])
- print
- #print _('%(number)d updates available. Size: %(size)s') % \
- # { 'number': ul.updates_available(),
- # 'size': _humanize_size(ul.updates_size()) }
-
-###############################################################################
-# Self-test code.
-def _main():
- """Self-test."""
- update_list = UpdateList()
- update_list.refresh_list()
- print_available(update_list)
- update_list.install_updates()
-
-if __name__ == '__main__':
- _main()
+ 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
index 0b5062e..9a77743 100644
--- a/extensions/cpsection/updater/view.py
+++ b/extensions/cpsection/updater/view.py
@@ -1,3 +1,6 @@
+# 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
@@ -12,381 +15,368 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-import logging
-import gtk
-import gobject
-from threading import Thread
from gettext import gettext as _
from gettext import ngettext
+import locale
-from sugar.graphics import style
-from jarabe.controlpanel.sectionview import SectionView
+import gobject
+import gtk
-import model
+from sugar.graphics import style
+from sugar.graphics.icon import Icon, CellRendererIcon
-gtk.gdk.threads_init()
+from jarabe.controlpanel.sectionview import SectionView
-_e = gobject.markup_escape_text
+from model import UpdateModel
_DEBUG_VIEW_ALL = True
+
class ActivityUpdater(SectionView):
- def __init__(self, modelwrapper, alerts):
+
+ 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)
- # top label #
self._top_label = gtk.Label()
self._top_label.set_line_wrap(True)
self._top_label.set_justify(gtk.JUSTIFY_LEFT)
- self._top_label.set_property('xalign', 0)
+ 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 #
bottom_label = gtk.Label()
bottom_label.set_line_wrap(True)
bottom_label.set_justify(gtk.JUSTIFY_LEFT)
- bottom_label.set_property('xalign', 0)
+ bottom_label.props.xalign = 0
bottom_label.set_markup(
- _('Software updates correct errors, eliminate security' \
+ _('Software updates correct errors, eliminate security ' \
'vulnerabilities, and provide new features.'))
-
- # main canvas #
- self.pack_start(self._top_label, expand=False)
- self.pack_start(gtk.HSeparator(), expand=False)
self.pack_start(bottom_label, expand=False)
+ bottom_label.show()
- # bundle pane #
- self.bundle_list = model.UpdateList()
- self.bundle_pane = BundlePane(self)
- self.pack_start(self.bundle_pane, expand=True)
-
- # progress pane #
- self.progress_pane = ProgressPane(self)
- self.pack_start(self.progress_pane, expand=True, fill=False)
-
- self.show_all()
-
- self.refresh_cb(None, None)
-
- # refresh #
- def refresh_cb(self, widget, event):
- self._top_label.set_markup('<big>%s</big>' % \
- _('Checking for updates...'))
- self.progress_pane.switch_to_check_progress()
- self.bundle_list.freeze_notify()
- Thread(target=self._do_refresh).start()
-
- def _do_refresh(self): #@inhibit_suspend
- self.bundle_list.refresh_list(self._refresh_progress_cb)
- gobject.idle_add(self._refresh_done_cb)
-
- def _refresh_progress_cb(self, n, extra=None):
- gobject.idle_add(self._progress, n, extra)
-
- # refresh done #
- def _refresh_done_cb(self):
- """Invoked in main thread when the refresh is complete."""
- self.bundle_list.thaw_notify()
- avail = self.bundle_list.updates_available()
- if avail == 0:
- header = _("Your software is up-to-date")
- self.progress_pane.switch_to_complete_message()
- else:
- header = ngettext("You can install %s update",
- "You can install %s updates", avail) \
- % avail
- self.bundle_pane.switch()
- self._top_label.set_markup('<big>%s</big>' % _e(header))
- self.bundle_pane.refresh_update_size()
-
- def install_clicked_cb(self, widget, event, data=None):
- """Invoked when the 'ok' button is clicked."""
- self._top_label.set_markup('<big>%s</big>' %
- _('Installing updates...'))
- self.progress_pane.switch_to_download_progress()
- self.bundle_list.freeze_notify()
- Thread(target=self._do_install).start()
-
- def _do_install(self): #@inhibit_suspend
- installed = self.bundle_list.install_updates(self._refresh_progress_cb)
- gobject.idle_add(self._install_done, installed)
-
- def _install_done(self, installed):
- self.bundle_list.thaw_notify()
- header = ngettext("%s update was installed",
- "%s updates were installed", installed) \
- % installed
- self._top_label.set_markup('<big>%s</big>' % _e(header))
- self.progress_pane.update(0)
- self.progress_pane.switch_to_complete_message()
-
- def _progress(self, n, extra=None, icon=None):
- """Invoked in main thread during a refresh operation."""
- self.progress_pane.update(n, extra, icon)
-
- def cancel_cb(self, widget, event, data=None):
- """Callback when the cancel button is clicked."""
- self.bundle_list.cancel()
- self.progress_pane.cancelling()
-
-class BundlePane(gtk.VBox):
- """Container for the activity and group lists."""
-
- def __init__(self, update_activity):
- gtk.VBox.__init__(self)
- self._updater_activity = update_activity
- self.set_spacing(style.DEFAULT_PADDING)
+ self._update_box = None
+ self._progress_pane = None
- # activity list #
- vpaned = gtk.VPaned()
- bundles = BundleListView(update_activity, self)
- vpaned.pack1(bundles, resize=True, shrink=False)
- self.pack_start(vpaned, expand=True)
-
- # Install/refresh buttons #
- button_box = gtk.HBox()
- button_box.set_spacing(style.DEFAULT_SPACING)
- hbox = gtk.HBox()
- hbox.pack_end(button_box, expand=False)
- self._size_label = gtk.Label()
- self._size_label.set_property('xalign', 0)
- self._size_label.set_justify(gtk.JUSTIFY_LEFT)
- hbox.pack_start(self._size_label, expand=True)
- self.pack_end(hbox, expand=False)
- check_button = gtk.Button(stock=gtk.STOCK_REFRESH)
- check_button.connect('clicked', update_activity.refresh_cb, self)
- button_box.pack_start(check_button, expand=False)
-
- self._install_button = _make_button(_("Install selected"),
- name='emblem-downloads')
- self._install_button.connect('clicked',
- update_activity.install_clicked_cb, self)
- button_box.pack_start(self._install_button, expand=False)
-
- def is_valid_cb(bundle_list, __):
- self._install_button.set_sensitive(bundle_list.is_valid())
- update_activity.bundle_list.connect('notify::is-valid', is_valid_cb)
-
- def refresh_update_size(self):
- """Update the 'download size' label."""
- bundle_list = self._updater_activity.bundle_list
- size = bundle_list.updates_size()
- self._size_label.set_markup(_('Download size: %s') %
- model._humanize_size(size))
- self._install_button.set_sensitive(bundle_list.updates_selected()!=0)
-
- def switch(self):
- """Make the bundle list visible and the progress pane invisible."""
- for widget, v in [(self, True),
- (self._updater_activity.progress_pane, False)]:# ,
- #(self.activity_updater.expander, False)]:
- widget.set_property('visible', v)
-
-class BundleListView(gtk.ScrolledWindow):
- """List view at the top, showing activities, versions, and sizes."""
- def __init__(self, update_activity, bundle_pane):
- gtk.ScrolledWindow.__init__(self)
- self._update_activity = update_activity
- self._bundle_pane = bundle_pane
-
- # create TreeView using a filtered treestore
- self._treeview = gtk.TreeView(self._update_activity.bundle_list)
-
- # toggle cell renderers #
- crbool = gtk.CellRendererToggle()
- crbool.set_property('activatable', True)
- crbool.set_property('xpad', style.DEFAULT_PADDING)
- crbool.set_property('indicator_size', style.zoom(26))
- crbool.connect('toggled', self.__toggled_cb)
-
- # icon cell renderers #
- cricon = gtk.CellRendererPixbuf()
- cricon.set_property('width', style.STANDARD_ICON_SIZE)
- cricon.set_property('height', style.STANDARD_ICON_SIZE)
-
- # text cell renderers #
- crtext = gtk.CellRendererText()
- crtext.set_property('xpad', style.DEFAULT_PADDING)
- crtext.set_property('ypad', style.DEFAULT_PADDING)
-
- #create the TreeViewColumn to display the data
- def view_func_maker(propname):
- def view_func(cell_layout, renderer, m, it):
- renderer.set_property(propname,
- not m.get_value(it, model.IS_HEADER))
- return view_func
- hide_func = view_func_maker('visible')
- insens_func = view_func_maker('sensitive')
- column_install = gtk.TreeViewColumn('Install', crbool)
- column_install.add_attribute(crbool, 'active',
- model.UPDATE_SELECTED)
- column_install.set_cell_data_func(crbool, hide_func)
- column = gtk.TreeViewColumn('Name')
- column.pack_start(cricon, expand=False)
- column.pack_start(crtext, expand=True)
- column.add_attribute(cricon, 'pixbuf', model.ICON)
- column.set_resizable(True)
- column.set_cell_data_func(cricon, hide_func)
- def markup_func(cell_layout, renderer, m, it):
- s = '<b>%s</b>' % _e(m.get_value(it, model.NAME))
- if m.get_value(it, model.IS_HEADER):
- s = '<big>%s</big>' % s
- desc = m.get_value(it, model.DESCRIPTION)
- if desc is not None and desc != '':
- s += '\n<small>%s</small>' % _e(desc)
- renderer.set_property('markup', s)
- insens_func(cell_layout, renderer, m, it)
- column.set_cell_data_func(crtext, markup_func)
-
- # add tvcolumn to treeview
- self._treeview.append_column(column_install)
- self._treeview.append_column(column)
-
- self._treeview.set_reorderable(False)
- self._treeview.set_enable_search(False)
- self._treeview.set_headers_visible(False)
- self._treeview.set_rules_hint(True)
- self._treeview.connect('button-press-event',
- self.__button_press_event_cb)
-
- def is_valid_cb(activity_list, __):
- self._treeview.set_sensitive(
- self._update_activity.bundle_list.is_valid())
- self._update_activity.bundle_list.connect('notify::is-valid',
- is_valid_cb)
- is_valid_cb(self._update_activity.bundle_list, None)
-
- self.add_with_viewport(self._treeview)
- self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-
- def __toggled_cb(self, crbool, path):
- row = self._treeview.props.model[path]
- row[model.UPDATE_SELECTED] = not row[model.UPDATE_SELECTED]
- self._bundle_pane.refresh_update_size()
-
- def __button_press_event_cb(self, widget, event):
- """
- Show a context menu if a right click was performed on an update entry
- """
- if not (event.type == gtk.gdk.BUTTON_PRESS and event.button == 3):
+ self._refresh()
+
+ def _switch_to_update_box(self):
+ if self._update_box in self.get_children():
return
- menu = gtk.Menu()
+ if self._progress_pane in self.get_children():
+ self.remove(self._progress_pane)
+ self._progress_pane = None
- item = gtk.MenuItem(_("_Uncheck All"))
- item.connect("activate", self.__check_activate_cb, False)
- if self._update_activity.bundle_list.updates_available() == 0:
- item.set_property("sensitive", False)
- menu.add(item)
+ 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)
- item = gtk.MenuItem(_("_Check All"))
- item.connect("activate", self.__check_activate_cb, True)
- if self._update_activity.bundle_list.updates_available() == 0:
- item.set_property("sensitive", False)
- menu.add(item)
+ self.pack_start(self._update_box, expand=True, fill=True)
+ self._update_box.show()
- menu.popup(None, None, None, 0, event.time)
- menu.show_all()
+ 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()
- def __check_activate_cb(self, sender, state):
- for i in self._update_activity.bundle_list:
- i[model.UPDATE_SELECTED] = state
- self._bundle_pane.refresh_update_size()
class ProgressPane(gtk.VBox):
- """Container which replaces the `ActivityPane` during refresh or
- install."""
+ '''Container which replaces the `ActivityPane` during refresh or
+ install.'''
- def __init__(self, update_activity):
- self._update_activity = update_activity
+ 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._icon = gtk.Image()
- self._icon.set_property('height-request', style.STANDARD_ICON_SIZE)
- # make an HBox to center the various buttons.
- hbox = gtk.HBox()
- self._cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
- self._refresh_button = gtk.Button(stock=gtk.STOCK_REFRESH)
- self._try_again_button = _make_button(_('Try again'),
- stock=gtk.STOCK_REFRESH)
- for widget, cb in [(self._cancel_button, update_activity.cancel_cb),
- (self._refresh_button, update_activity.refresh_cb),
- (self._try_again_button, update_activity.refresh_cb)]:
- widget.connect('clicked', cb, update_activity)
- hbox.pack_start(widget, expand=True, fill=False)
-
- self.pack_start(self._icon)
- self.pack_start(self._progress)
+ style.COLOR_BUTTON_GREY.get_gdk_color())
self.pack_start(self._label)
- self.pack_start(hbox)
+ self._label.show()
- def update(self, n, extra=None, icon=None):
- """Update the status of the progress pane. `n` should be a float
- in [0, 1], or else None. You can optionally provide extra information
- in `extra` or an icon in `icon`."""
+ alignment_box = gtk.Alignment(xalign=0.5, yalign=0.5)
+ self.pack_start(alignment_box)
+ alignment_box.show()
- if n is None:
- self._progress.pulse()
- else:
- self._progress.set_fraction(n)
- extra = _e(extra) if extra is not None else ''
- self._label.set_markup(extra)
- self._icon.set_property('visible', icon is not None)
- if icon is not None:
- self._icon.set_from_pixbuf(icon)
-
- def switch_to_check_progress(self):
- self._switch(show_cancel=True, show_bar=True)
- self._label.set_markup(_('Checking for updates...'))
-
- def switch_to_download_progress(self):
- self._switch(show_cancel=True, show_bar=True)
- self._label.set_markup(_('Starting download...'))
-
- def switch_to_complete_message(self):
- self._switch(show_cancel=False, show_bar=False)
- self._label.set_markup('')
-
- def cancelling(self):
- self._cancel_button.set_sensitive(False)
- self._label.set_markup(_('Cancelling...'))
-
- def _switch(self, show_cancel, show_bar, show_try_again=False):
- """Make the progress pane visible and the activity pane invisible."""
- self._update_activity.bundle_pane.set_property('visible', False)
- self.set_property('visible', True)
- for widget, v in [(self._progress, show_bar),
- (self._cancel_button, show_cancel),
- (self._refresh_button,
- not show_cancel and not show_try_again),
- (self._try_again_button, show_try_again),
- #(self._update_activity.expander, False)
- ]:
- widget.set_property('visible', v)
- self._cancel_button.set_sensitive(True)
- #self._update_activity.expander.set_expanded(False)
-
-def _make_button(label_text, stock=None, name=None):
- """Convenience function to make labelled buttons with images."""
- b = gtk.Button()
- hbox = gtk.HBox()
- hbox.set_spacing(style.DEFAULT_PADDING)
- i = gtk.Image()
- if stock is not None:
- i.set_from_stock(stock, gtk.ICON_SIZE_BUTTON)
- if name is not None:
- i.set_from_icon_name(name, gtk.ICON_SIZE_BUTTON)
- hbox.pack_start(i, expand=False)
- l = gtk.Label(label_text)
- hbox.pack_start(l, expand=False)
- b.add(hbox)
- return b
+ 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)