From c53240374b795ac4ea70fc506987b215ad016569 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Thu, 30 Jul 2009 09:00:22 +0000 Subject: Add updater component --- (limited to 'extensions') 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:: + + + + + + + + + + + + 7 + + + {3ca105e0-2280-4897-99a0-c277d1b733d2} + 0.82 + 0.84 + http://activities.sugarlabs.org/downloads/file/25986/bounce-7.xo + + sha256:816a7c43b4f1ea4769c61c03fea24842ec5fa566b7d41626ffc52ec37b37b6c5 + + + +""" + +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('%s' % _('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('%s' % _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('%s' % + _('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('%s' % _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 = '%s' % _e(m.get_value(it, model.NAME)) + if m.get_value(it, model.IS_HEADER): + s = '%s' % s + desc = m.get_value(it, model.DESCRIPTION) + if desc is not None and desc != '': + s += '\n%s' % _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() -- cgit v0.9.1