Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/extensions
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@member.fsf.org>2009-07-30 09:00:22 (GMT)
committer Aleksey Lim <alsroot@member.fsf.org>2009-07-30 09:00:22 (GMT)
commitc53240374b795ac4ea70fc506987b215ad016569 (patch)
treea32b5d89b30d67e81211a9e21786aa36c59a104d /extensions
parent314d18604cf916093b067d6180edc014586fe994 (diff)
Add updater component
Diffstat (limited to 'extensions')
-rw-r--r--extensions/cpsection/updater/NEWS1
-rw-r--r--extensions/cpsection/updater/README1
-rw-r--r--extensions/cpsection/updater/SOURCES1
-rw-r--r--extensions/cpsection/updater/TODO2
-rw-r--r--extensions/cpsection/updater/__init__.py22
-rw-r--r--extensions/cpsection/updater/backends/__init__.py18
-rw-r--r--extensions/cpsection/updater/backends/aslo.py108
-rwxr-xr-xextensions/cpsection/updater/model.py289
-rw-r--r--extensions/cpsection/updater/view.py379
9 files changed, 821 insertions, 0 deletions
diff --git a/extensions/cpsection/updater/NEWS b/extensions/cpsection/updater/NEWS
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/extensions/cpsection/updater/NEWS
@@ -0,0 +1 @@
+
diff --git a/extensions/cpsection/updater/README b/extensions/cpsection/updater/README
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/extensions/cpsection/updater/README
@@ -0,0 +1 @@
+
diff --git a/extensions/cpsection/updater/SOURCES b/extensions/cpsection/updater/SOURCES
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/extensions/cpsection/updater/SOURCES
@@ -0,0 +1 @@
+
diff --git a/extensions/cpsection/updater/TODO b/extensions/cpsection/updater/TODO
new file mode 100644
index 0000000..d343113
--- /dev/null
+++ b/extensions/cpsection/updater/TODO
@@ -0,0 +1,2 @@
+Fix Icon
+FIX Maniest
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/__init__.py b/extensions/cpsection/updater/backends/__init__.py
new file mode 100644
index 0000000..c148fcc
--- /dev/null
+++ b/extensions/cpsection/updater/backends/__init__.py
@@ -0,0 +1,18 @@
+#!/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..7985e5c
--- /dev/null
+++ b/extensions/cpsection/updater/backends/aslo.py
@@ -0,0 +1,108 @@
+#!/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://activities.sugarlabs.org/downloads/file/25986/bounce-7.xo</em:updateLink>
+
+ <em:updateHash>sha256:816a7c43b4f1ea4769c61c03fea24842ec5fa566b7d41626ffc52ec37b37b6c5</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+</RDF:Description></RDF:RDF>
+"""
+
+import urllib2
+from urllib2 import HTTPError
+
+import socket
+
+from xml.etree.ElementTree import ElementTree, XML
+
+from jarabe import config
+
+class ASLOParser():
+ """XML parser to pull out data expressed in our aslo format."""
+
+ def __init__(self, xml_data):
+ self.elem = XML(xml_data)
+
+ def parse(self):
+ try:
+ self.version = self.elem.find(".//{http://www.mozilla.org/2004/em-rdf#}version").text
+ self.link = self.elem.find(".//{http://www.mozilla.org/2004/em-rdf#}updateLink").text
+ self.size = self.elem.find(".//{http://www.mozilla.org/2004/em-rdf#}updateSize").text
+ self.size = long(self.size) * 1024
+ except:
+ self.version = 0
+ self.link = None
+ self.size = 0
+
+def parse_aslo(xml_data):
+ """Parse the activity information embedded in the given string
+ containing XML data. Returns a list containing the activity version and url.
+ """
+ ap = ASLOParser(xml_data)
+ ap.parse()
+ return ap.version, ap.link, ap.size
+
+def parse_url(url):
+ """Parse the activity information at the given URL. Returns the same
+ information as `parse_xml` does, and raises the same exceptions.
+ The `urlopen_args` can be any keyword arguments accepted by
+ `bitfrost.util.urlrange.urlopen`."""
+
+ response = urllib2.urlopen(url)
+ return parse_aslo(response.read())
+
+def fetch_update_info(bundle):
+ """Return a tuple of new version, url for new version.
+
+ All the information about the new version is `None` if no newer
+ update can be found.
+ """
+
+ url = 'http://activities.sugarlabs.org/services/update.php?id=' + bundle.get_bundle_id() + '&appVersion=' + config.version
+
+ return parse_url(url)
+
+#########################################################################
+# Self-test code.
+def _main():
+ """Self-test."""
+ print parse_url('http://activities.sugarlabs.org/services/update.php?id=bounce')
+
+if __name__ == '__main__': _main ()
diff --git a/extensions/cpsection/updater/model.py b/extensions/cpsection/updater/model.py
new file mode 100755
index 0000000..4382ff1
--- /dev/null
+++ b/extensions/cpsection/updater/model.py
@@ -0,0 +1,289 @@
+#!/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
+"""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
+
+import gettext
+_ = lambda msg: gettext.dgettext('sugar-update-control', msg)
+
+import gtk
+import gobject
+
+from jarabe.model import bundleregistry
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.datastore import datastore
+from backends import aslo
+
+#_logger = logging.getLogger('update-activity')
+
+##########################################################################
+# Fundamental data object.
+
+_column_name_map = dict(globals())
+
+"""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)
+
+
+"""Map column names to indices."""
+_column_name_map = dict((k,v) for k,v in globals().items()
+ if k not in _column_name_map and k!='_column_name_map')
+
+
+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),
+ 'saw_network_failure': (gobject.TYPE_BOOLEAN, 'saw network failure',
+ 'true iff at least one network IO error '+
+ 'occurred when the UpdateList was last '+
+ 'refreshed',
+ False, gobject.PARAM_READABLE),
+ 'saw_network_success': (gobject.TYPE_BOOLEAN, 'saw network success',
+ 'true iff at least one network operation '+
+ 'completed successfully when the UpdateList '+
+ 'was last refreshed',
+ False, gobject.PARAM_READABLE),
+ }
+
+ def __init__(self):
+ logging.debug('STARTUP: Loading the bundle updater')
+
+ gtk.ListStore.__init__(self,
+ str, object, gtk.gdk.Pixbuf, str,
+ long, long, long, str,
+ str, bool, bool, bool)
+
+ self._cancel = False
+ self._is_valid = True
+ self.registry =bundleregistry.get_registry()
+
+ 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.clear()
+ self.steps_total = len(self.registry._bundles)
+ self.steps_count = 0
+
+ row_map = {}
+
+ for bundle in self.registry._bundles:
+ self._make_progress(_('Checking %s...') % bundle.get_name())
+
+ if self._cancel:
+ break # Cancel bundle refresh
+
+ row = [None, None, None, None,
+ 0, 0, 0, None,
+ None, True, False, False]
+ row[BUNDLE] = bundle
+ row[BUNDLE_ID] = bundle.get_bundle_id()
+
+ if self.refresh_row(row):
+ row_map[row[BUNDLE_ID]] = self.get_path(self.append(row))
+
+ def cancel(self):
+ self._cancel = True
+
+ def refresh_row(self, row):
+ logging.debug('Looking for %s' % row[BUNDLE].get_name())
+
+ try:
+ new_version, new_url, new_size = aslo.fetch_update_info(row[BUNDLE])
+ except Exceptoin, e:
+ logging.warning('Failure %s updating: %s' % \
+ (row[BUNDLE].get_name(), e))
+ return False
+
+ row[CURRENT_VERSION] = row[BUNDLE].get_activity_version()
+ row[UPDATE_VERSION] = long(new_version)
+
+ 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:
+ urllib.urlretrieve(row[UPDATE_URL], xofile)
+
+ 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:
+ # TRANSLATORS: download size is 0
+ return _("None")
+ elif bytes < 1024:
+ # TRANSLATORS: download size of very small updates
+ return _("1 KB")
+ elif bytes < 1024 * 1024:
+ # TRANSLATORS: download size of small updates, e.g. "250 KB"
+ return locale.format(_("%.0f KB"), bytes/1024)
+ else:
+ # TRANSLATORS: 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
+ print
+ 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])
+ 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 ()
diff --git a/extensions/cpsection/updater/view.py b/extensions/cpsection/updater/view.py
new file mode 100644
index 0000000..ee773fb
--- /dev/null
+++ b/extensions/cpsection/updater/view.py
@@ -0,0 +1,379 @@
+from gettext import gettext as _
+import gettext
+import logging
+from threading import Thread
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+
+from sugar.activity import activity
+from sugar.graphics import style
+from jarabe.controlpanel.sectionview import SectionView
+
+import model
+from model import _humanize_size
+
+gtk.gdk.threads_init()
+
+_logger = logging.getLogger('update-activity')
+
+_e = gobject.markup_escape_text
+
+_DEBUG_VIEW_ALL = True
+
+class ActivityUpdater(SectionView):
+ def __init__(self, modelwrapper, alerts):
+ SectionView.__init__(self)
+ 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)
+
+ # 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.set_markup(_('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)
+
+ # 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_cb, 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 = gettext.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_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_cb, installed)
+
+ def _install_done_cb(self, installed):
+ self.bundle_list.thaw_notify()
+ header = gettext.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 _install_progress_cb(n, extra=None, icon=None):
+ gobject.idle_add(self._progress_cb, n, extra, icon)
+
+ def _progress_cb(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)
+
+ # activity list #
+ vpaned = gtk.VPaned()
+ self.bundles = BundleListView(update_activity, self)
+ vpaned.pack1(self.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)
+ self.check_button = gtk.Button(stock=gtk.STOCK_REFRESH)
+ self.check_button.connect('clicked', update_activity.refresh_cb, self)
+ button_box.pack_start(self.check_button, expand=False)
+ self.install_button = _make_button(_("Install selected"),
+ name='emblem-downloads')
+ self.install_button.connect('clicked', update_activity.install_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') %
+ _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')
+ self.column_install = gtk.TreeViewColumn('Install', crbool)
+ self.column_install.add_attribute(crbool, 'active', model.UPDATE_SELECTED)
+ self.column_install.set_cell_data_func(crbool, hide_func)
+ self.column = gtk.TreeViewColumn('Name')
+ self.column.pack_start(cricon, expand=False)
+ self.column.pack_start(crtext, expand=True)
+ self.column.add_attribute(cricon, 'pixbuf', model.ICON)
+ self.column.set_resizable(True)
+ self.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)
+ self.column.set_cell_data_func(crtext, markup_func)
+
+ # add tvcolumn to treeview
+ self.treeview.append_column(self.column_install)
+ self.treeview.append_column(self.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.show_context_menu)
+
+ 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 show_context_menu(self, widget, event):
+ """
+ Show a context menu if a right click was performed on an update entry
+ """
+ if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
+ def cb(__, f):
+ f()
+ self.bundle_pane._refresh_update_size()
+ menu = gtk.Menu()
+ item_select_none = gtk.MenuItem(_("_Uncheck All"))
+ item_select_none.connect("activate", cb,
+ bundle_list.unselect_all)
+ menu.add(item_select_none)
+ if self.updater_activity.activity_list.updates_available() == 0:
+ item_select_none.set_property("sensitive", False)
+ item_select_all = gtk.MenuItem(_("_Check All"))
+ item_select_all.connect("activate", cb,
+ bundle_list.select_all)
+ menu.add(item_select_all)
+ menu.popup(None, None, None, 0, event.time)
+ menu.show_all()
+ return True
+ return False
+
+class ProgressPane(gtk.VBox):
+ """Container which replaces the `ActivityPane` during refresh or
+ install."""
+
+ def __init__(self, update_activity):
+ self.update_activity = update_activity
+ gtk.VBox.__init__(self)
+ self.set_spacing(style.DEFAULT_PADDING)
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+
+ self.bar = gtk.ProgressBar()
+ 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.bar)
+ self.pack_start(self.label)
+ self.pack_start(hbox)
+
+ 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`."""
+
+ if n is None:
+ self.bar.pulse()
+ else:
+ self.bar.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.bar, 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
+
+if __name__ == "__main__":
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ t = UpdateActivity(window)
+ gtk.main()