[Commits] sugar-update-control branch master updated.
Daniel Drake
dsd at laptop.org
Fri Dec 5 17:56:23 EST 2008
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "/home/cscott/public_git/sugar-update-control".
The branch, master has been updated
via bf43f5062152b6d3bc3b15f53dc2d29fddea3146 (commit)
from 494c6e59a2d7558439ad30131dd3b72d0dff60d8 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
Makefile | 2 +-
bitfrost/update/actutils.py | 21 ++++++++++++---------
setup.py | 6 ++----
src/__init__.py | 23 +++++++++++++++++++++++
model/updater.py => src/model.py | 4 ++--
view/updater.py => src/view.py | 28 ++++++----------------------
sugar-update-control.spec | 2 +-
7 files changed, 47 insertions(+), 39 deletions(-)
delete mode 100644 bitfrost/update/__init__.py
create mode 100644 src/__init__.py
rename model/updater.py => src/model.py (99%)
rename view/updater.py => src/view.py (97%)
- Log -----------------------------------------------------------------
commit bf43f5062152b6d3bc3b15f53dc2d29fddea3146
Author: Daniel Drake <dsd at laptop.org>
Date: Fri Dec 5 18:18:23 2008 +0000
Update for sugar-0.83
Things moved around.
diff --git a/Makefile b/Makefile
index d14b73e..2e809cb 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ update-version:
grep -q $(VERSION_MAJOR).$(VERSION_MINOR) debian/changelog
# update the translation template
-po/sugar-update-control.pot: model/updater.py view/updater.py
+po/sugar-update-control.pot: src/__init__.py src/view.py src/model.py
xgettext -o $@ \
--copyright-holder="One Laptop per Child Association, Inc." \
--package-name="$(PACKAGE)" \
diff --git a/bitfrost/update/__init__.py b/bitfrost/update/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/bitfrost/update/actutils.py b/bitfrost/update/actutils.py
index 7e0efcc..1146064 100644
--- a/bitfrost/update/actutils.py
+++ b/bitfrost/update/actutils.py
@@ -111,10 +111,13 @@ class BundleHelper(object):
self.bundle = ContentBundle(filename)
self.is_activity = False
- def is_installed(self):
+ def is_installed(self, registry):
"""Return True if (an older version of) the bundle is already
installed."""
- return self.bundle.is_installed()
+ if self.is_activity:
+ return registry.is_installed(self.bundle)
+ else:
+ return self.bundle.is_installed()
def get_name(self):
"""Get the name of the activity or content from the bundle."""
@@ -134,7 +137,7 @@ class BundleHelper(object):
def _activity_install_or_upgrade(self, registry):
"""Install/upgrade an activity bundle, preserving favorites state."""
- if self.is_installed():
+ if self.is_installed(registry):
# upgraded activities are favorites if the pre-upgrade
# activity was a favorite.
ainfo = registry.get_activity(self.bundle.get_bundle_id())
@@ -145,12 +148,12 @@ class BundleHelper(object):
favorite = True
self.bundle.install()
# set favorites flag for the new bundle-id/version.
- registry.set_activity_favorite(self.bundle.get_bundle_id(),
- self.bundle.get_activity_version(),
- favorite)
- def _library_install_or_upgrade(self):
+ registry.set_bundle_favorite(self.bundle.get_bundle_id(),
+ self.bundle.get_activity_version(),
+ favorite)
+ def _library_install_or_upgrade(self, registry):
"""Install/upgrade a content bundle."""
- if self.is_installed():
+ if self.is_installed(registry):
self.bundle.uninstall()
self.bundle.install()
@@ -160,4 +163,4 @@ class BundleHelper(object):
if self.is_activity:
self._activity_install_or_upgrade(registry)
else:
- self._library_install_or_upgrade()
+ self._library_install_or_upgrade(registry)
diff --git a/model/updater.py b/model/updater.py
deleted file mode 100755
index 9851753..0000000
--- a/model/updater.py
+++ /dev/null
@@ -1,861 +0,0 @@
-#!/usr/bin/python2.5
-# Copyright (C) 2008 One Laptop Per Child Association, Inc.
-# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
-# Written by C. Scott Ananian <cscott at laptop.org>
-"""Activity updater: backing model.
-
-This module implements the non-GUI portions of the activity updater,
-including in particular the master list of groups, activities, whether
-updates are needed, and the URL at which to find the updated activity.
-
-Because `UpdateList` inherits from `gtk.ListStore` so that it plays
-nicely with a GUI, this module requires `gtk`. Those of you without
-DISPLAY set will have to put up with a GtkWarning that the display
-couldn't be opened. Sorry.
-"""
-from __future__ import with_statement
-from __future__ import division
-
-# for testing
-_DEBUG_MAKE_ALL_OLD = False
-_DEBUG_CHECK_VERSIONS = False
-
-# default timeout for HTTP connections, in seconds
-HTTP_TIMEOUT=30
-
-import gtk
-import gobject
-
-import locale
-import os
-import os.path
-import socket
-import sys
-import traceback
-import zipfile
-from gettext import gettext as _
-from HTMLParser import HTMLParseError
-from urllib2 import HTTPError
-
-import bitfrost.update.actinfo as actinfo
-import bitfrost.update.actutils as actutils
-import bitfrost.update.microformat as microformat
-import bitfrost.util.urlrange as urlrange
-
-# weak dependency on inhibit_suspend from olpc-update package
-try:
- from bitfrost.update import inhibit_suspend
-except ImportError:
- # use a no-op substitude for the inhibit_suspend decorator.
- inhibit_suspend = lambda f: f
-
-# lifted from gnome-update-manager/Common/utils
-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 _svg2pixbuf(icon_data):
- """Convert the given `icon_data` SVG string to a `gtk.gdk.Pixbuf`
- with maximum size 55x55."""
- import re, rsvg
- # substitute black/white for icon color entities.
- for entity, value in [('stroke_color','#808080'),
- ('fill_color','#eee')]:
- xml = '<!ENTITY %s "%s">' % (entity, value)
- icon_data = re.sub('<!ENTITY %s .*>' % entity, xml, icon_data)
- h = rsvg.Handle(data=icon_data)
- if h.get_property('width') > 55 or h.get_property('height') > 55:
- # lame! scale it.
- print "WARNING: oversize icon (%dx%d), scaling." % \
- (h.get_property('width'), h.get_property('height'))
- del h
- pbl = gtk.gdk.PixbufLoader()
- pbl.set_size(55, 55)
- pbl.write(icon_data)
- pbl.close()
- return pbl.get_pixbuf()
- return h.get_pixbuf()
-
-_parse_cache = {}
-def _check_for_updates(url, activity_id):
- """Downloads the given URL, parses it (caching the result), and returns
- a list of (version, url) pairs present for the given `activity_id`.
- Returns `None` if there was a problem downloading the URL.
- Returns a zero-length list if the given URL is unparsable or does not
- contain information for the desired activity_id."""
- global _parse_cache
- if url not in _parse_cache:
- try:
- __, __, _parse_cache[url] = \
- microformat.parse_url(url, timeout=HTTP_TIMEOUT)
- if _DEBUG_CHECK_VERSIONS:
- # for kicks and giggles, verify these version #s!
- for n_activity_id, versions in _parse_cache[url].items():
- for ver, url2 in versions:
- actual_id, actual_ver = \
- actutils.id_and_version_from_url(url2)
- if actual_id != n_activity_id:
- print "ACTIVITY ID SHOULD BE", n_activity_id, \
- "BUT ACTUALLY IS", actual_id, ("(%s)"%url2)
- if actual_ver != ver:
- print "VERSION SHOULD BE", ver, \
- "BUT ACTUALLY IS", actual_ver, ("(%s)"%url2)
- except HTMLParseError:
- _parse_cache[url] = {} # parse error
- except (IOError, socket.error):
- _parse_cache[url] = None # network error
- activity_map = _parse_cache[url]
- if activity_map is None: return None # error attempting to check.
- if activity_id not in activity_map: return [] # no versions found.
- return activity_map[activity_id]
-
-def _retrieve_update_version(actbun,
- network_success_cb=(lambda url: None),
- network_failure_cb=(lambda url: None)):
- """Return a tuple of current version, new version, url for new
- version, size of new version, given a current activity bundle.
- All the information about the new version is `None` if no newer
- update can be found. The optional `network_success_cb` is invoked
- with a single `url` parameter whenever a network operation
- succeeds, and the optional `network_failure_cb` is invoked with a
- single `url` parameter if all fallback network operations for a
- given activity bundle fail."""
- update_url = actbun.get_update_url()
- oldv = 0 if _DEBUG_MAKE_ALL_OLD else actbun.get_activity_version()
- # here we implement the search for a release-specific version
- # XXX: would be nice to use retrieve_first_variant here
- vlist = None
- for uu in actinfo.url_variants(update_url):
- vlist = _check_for_updates(uu, actbun.get_bundle_id())
- if vlist is not None:
- network_success_cb(uu)
- if len(vlist) > 0:
- break # found some info
- if vlist is None:
- # all url variants for the given actbun have failed.
- network_failure_cb(update_url)
- return oldv, None, None, 0
- newv, newu = microformat.only_best_update(vlist + [(oldv, None)])
- if newu is None or newv==oldv: return oldv, None, None, 0 # no updates
- # find size of new version.
- try:
- size = urlrange.urlopen(newu, timeout=HTTP_TIMEOUT).length()
- return oldv, newv, newu, size
- except (HTTPError, IOError, socket.error):
- # hmm, i guess that url isn't valid after all. bail.
- return oldv, None, None, 0 # there are no *actual* updates.
-
-##########################################################################
-# Fundamental data object: the real work gets done here.
-
-_column_name_map = dict(globals())
-ACTIVITY_ID, ACTIVITY_BUNDLE, ACTIVITY_ICON, \
- UPDATE_URL, UPDATE_VERSION, UPDATE_SIZE, \
- UPDATE_EXISTS, UPDATE_SELECTED, \
- DESCRIPTION_BIG, DESCRIPTION_SMALL, IS_HEADER, GROUP_NUM = \
- xrange(12)
-"""List of columns in the `UpdateList`."""
-_column_name_map = dict((k,v) for k,v in globals().items()
- if k not in _column_name_map and k!='_column_name_map')
-"""Mapping from column names to indices."""
-
-class UpdateList(gtk.ListStore):
- """Model which provides backing storage for the activity 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, skip_icons=False):
- gtk.ListStore.__init__(self,
- # column types
- str, object, gtk.gdk.Pixbuf,
- str, long, long,
- bool, bool, str, str, bool, int)
- self._skip_icons = skip_icons
- self._is_valid = False
- self._network_failures = []
- self._saw_network_success = False
-
- def __del__(self):
- """Free up any memory held by the cache in urlrange."""
- urlrange.urlcleanup()
-
- def _append(self, at=None, **kwargs):
- """Utility function to make it easier to add rows and get paths."""
- global _column_name_map
- # defaults for each column
- row = [None, None, None,
- None, 0, 0,
- True, True, None, None, False, 0]
- # set entries in the row based on kwargs
- for k,v in kwargs.items():
- row[_column_name_map[k]] = v
- if at is not None:
- it = self.insert(at, row)
- else:
- it = self.append(row)
- return self.get_path(it)
-
- def toggle_select(self, path):
- """Toggle whether the given update will be installed."""
- row = self[path]
- row[UPDATE_SELECTED] = not row[UPDATE_SELECTED]
-
- # don't touch the UI in refresh, it needs to be thread-safe.
- # (model will be disconnected from the view before invoking refresh
- # in another thread)
- def refresh(self, progress_callback=lambda n, extra: None,
- clear_cache=True):
- """Perform network operations to find available updates.
-
- The `progress_callback` is invoked with numbers between 0 and 1
- or `None` as the network queries complete. The last callback will be
- `progress_callback(1)`. Passing `None` to `progress_callback`
- requests "pulse mode" from the progress bar.
- """
- global _parse_cache
- if clear_cache:
- _parse_cache = {} # clear microformat parse cache
- urlrange.urlcleanup() # clean url cache
- self._cancel = False
- self._invalidate()
- # don't notify for the following; we'll notify at the end when we
- # know what the new values ought to be.
- self._saw_network_success = False
- self._network_failures = []
- # bookkeeping
- progress_callback(None, None)
- self.clear()
- # find all activities already installed.
- progress_callback(None, _('Looking for local activities and content...')) # pulse
- activities = actinfo.get_activities() + actinfo.get_libraries()
- # enumerate all group urls
- progress_callback(None, _('Loading groups...'))
- group_urls = actinfo.get_activity_group_urls()
- # now we've got enough information to allow us to compute a
- # reasonable completion percentage.
- steps_total = [ len(activities) + len(group_urls) + 3 ]
- steps_count = [ 0 ] # box this to allow update from mkprog.
- def mkprog(msg=None):
- """Helper function to do progress update."""
- steps_count[0] += 1
- progress_callback(steps_count[0]/steps_total[0], msg)
- mkprog(_('Loading groups...'))
- # okay, first load up any group definitions; these take precedence
- # if present.
- groups = []
- def group_parser(f, url):
- name, desc, groups = microformat.parse_html(f.read(), url)
- if len(groups) > 0 or (name is not None and desc is not None):
- return name, desc, groups
- return None # hmm, not a successful parse.
- for gurl in group_urls:
- mkprog(_('Fetching %s...') % gurl)
- if self._cancel: break # bail!
- gdata = actinfo.retrieve_first_variant(gurl, group_parser,
- timeout=HTTP_TIMEOUT)
- if gdata is not None:
- gname, gdesc, gactmap = gdata
- groups.append((gname, gdesc, gurl, gactmap))
- self._saw_network_success = True
- else:
- # headers even for failed groups.
- groups.append((None, gurl, gurl, {}))
- self._network_failures.append(gurl)
- # now start filling up the liststore, keeping a map from activity id
- # to liststore path
- row_map = {}
- group_num = 0
- for gname, gdesc, gurl, gactmap in groups:
- # add group header.
- if gname is None: gname = _('Activity Group')
- self._append(IS_HEADER=True,
- UPDATE_URL=gurl,
- GROUP_NUM=group_num,
- DESCRIPTION_BIG=gname,
- DESCRIPTION_SMALL=gdesc)
- # now add entries for all activities in the group, whether
- # currently installed or not.
- for act_id, version_list in sorted(gactmap.items()):
- version, url = microformat.only_best_update(version_list)
- if act_id not in row_map:
- # temporary description in case user cancels the refresh
- tmp_desc = act_id.replace('sugar-is-lame',
- 'lame-is-the-new-cool')
- row_map[act_id] = self._append(ACTIVITY_ID=act_id,
- GROUP_NUM=group_num,
- UPDATE_EXISTS=True,
- UPDATE_URL=url,
- UPDATE_VERSION=version,
- DESCRIPTION_BIG=tmp_desc)
- steps_total[0] += 1 # new activity?
- else:
- # allow for a later version in a different group
- row = self[row_map[act_id]]
- if version > row[UPDATE_VERSION]:
- row[UPDATE_URL] = url
- # XXX: deal with pinned updates.
- group_num += 1
- # add in information from local activities.
- self._append(IS_HEADER=True, GROUP_NUM=group_num,
- DESCRIPTION_BIG=_('Local activities'))
- for act in activities:
- act_id = act.get_bundle_id()
- if act_id not in row_map:
- row_map[act_id] = self._append(ACTIVITY_ID=act_id,
- GROUP_NUM=group_num,
- UPDATE_EXISTS=False)
- else:
- steps_total[0] -= 1 # correct double-counting.
- # update icon, and bundle
- row = self[row_map[act_id]]
- row[ACTIVITY_BUNDLE] = act
- row[DESCRIPTION_BIG] = act.get_name()
- if not self._skip_icons:
- try:
- row[ACTIVITY_ICON] = _svg2pixbuf(act.get_icon_data())
- except IOError:
- # dlo trac #8149: don't kill updater if existing icon
- # bundle is malformed.
- pass
- group_num += 1
- # now do extra network traffic to look for actual updates.
- def refresh_existing(row):
- """Look for updates to an existing activity."""
- act = row[ACTIVITY_BUNDLE]
- def net_good(url_): self._saw_network_success = True
- def net_bad(url): self._network_failures.append(url)
- oldver, newver, newurl, size = \
- _retrieve_update_version(act, net_good, net_bad)
- # merge w/ info from the activity group
- # (activity group entries have UPDATE_EXISTS=True)
- if row[UPDATE_EXISTS] and \
- ((act.get_activity_version() if newver is None else newver) \
- < row[UPDATE_VERSION]):
- newver, newurl = row[UPDATE_VERSION], row[UPDATE_URL]
- size = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)\
- .length()
- row[UPDATE_EXISTS] = (newver is not None)
- row[UPDATE_URL] = newurl
- row[UPDATE_SIZE] = size
- if newver is None:
- description = _('At version %s') % oldver
- else:
- description = \
- _('From version %(old)d to %(new)d (Size: %(size)s)') % \
- { 'old':oldver, 'new':newver, 'size':_humanize_size(size) }
- row[UPDATE_SELECTED] = True
- row[DESCRIPTION_SMALL] = description
- def refresh_new(row):
- """Look for updates to a new activity in the group."""
- uo = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)
- row[UPDATE_SIZE] = uo.length()
- zf = zipfile.ZipFile(uo)
- # grab data from activity.info file
- activity_base = actutils.bundle_base_from_zipfile(zf)
- try:
- zf.getinfo('%s/activity/activity.info' % activity_base)
- is_activity = True
- except KeyError:
- is_activity = False
- if is_activity:
- cp = actutils.activity_info_from_zipfile(zf)
- SECTION = 'Activity'
- else:
- cp = actutils.library_info_from_zipfile(zf)
- SECTION = 'Library'
- act_id = None
- for fieldname in ('bundle_id', 'service_name', 'global_name'):
- if cp.has_option(SECTION, fieldname):
- act_id = cp.get(SECTION, fieldname)
- break
- if not act_id:
- raise RuntimeError("bundle_id not found for %s" %
- row[UPDATE_URL])
- name = act_id
- if cp.has_option(SECTION, 'name'):
- name = cp.get(SECTION, 'name')
- # okay, try to get an appropriately translated name.
- if is_activity:
- lcp = actutils.locale_activity_info_from_zipfile(zf)
- if lcp is not None:
- name = lcp.get(SECTION, 'name')
- else:
- s = actutils.locale_section_for_content_bundle(cp)
- if s is not None and cp.has_option(s, 'name'):
- name = cp.get(s, 'name')
- version = None
- for fieldname in ('activity_version', 'library_version'):
- if cp.has_option(SECTION, fieldname):
- version = int(cp.get(SECTION, fieldname))
- break
- if version is None:
- raise RuntimeError("can't find version for %s" %
- row[UPDATE_URL])
- row[DESCRIPTION_BIG] = name
- row[DESCRIPTION_SMALL] = \
- _('New version %(version)s (Size: %(size)s)') % \
- {'version':version, 'size':_humanize_size(row[UPDATE_SIZE])}
- # okay, let's try to update the icon!
- if not self._skip_icons:
- if is_activity:
- # XXX should failures here kill the upgrade?
- icon_file = cp.get(SECTION, 'icon')
- icon_filename = '%s/activity/%s.svg'%(activity_base, icon_file)
- row[ACTIVITY_ICON] = _svg2pixbuf(zf.read(icon_filename))
- else:
- row[ACTIVITY_ICON] = _svg2pixbuf(actinfo.DEFAULT_LIBRARY_ICON)
- # go through activities and do network traffic
- for row in self:
- if self._cancel: break # bail!
- if row[IS_HEADER]: continue # skip
- mkprog(_('Checking %s...') % row[DESCRIPTION_BIG])
- try:
- if row[ACTIVITY_BUNDLE] is None:
- refresh_new(row)
- self._saw_network_success = True
- else:
- refresh_existing(row)
- except:
- row[UPDATE_EXISTS] = False # something wrong, can't update
- if row[UPDATE_URL] is not None:
- self._network_failures.append(row[UPDATE_URL])
- # log the problem for later debugging.
- print "Failure updating", row[DESCRIPTION_BIG], \
- row[DESCRIPTION_SMALL], row[UPDATE_URL]
- traceback.print_exc()
- mkprog('Sorting...') # all done
- # hide headers if all children are hidden
- sawone, last_header = False, None
- for row in self:
- if row[IS_HEADER]:
- if last_header is not None:
- last_header[UPDATE_EXISTS] = sawone
- sawone, last_header = False, row
- elif row[UPDATE_EXISTS]:
- sawone = True
- if last_header is not None:
- last_header[UPDATE_EXISTS] = sawone
- # finally, sort all rows.
- self._sort()
- mkprog() # all done
- # XXX: check for base os update, and add an entry here?
- self._is_valid = True
- self.notify('is-valid')
- self.notify('saw-network-failure')
- self.notify('saw-network-success')
-
- def _sort(self):
- """Sort rows by group number, then case-insensitively by description."""
- l = lambda s: s.decode('utf-8','ignore').lower() \
- if s is not None else None
- def sort_value(row_num):
- row = self[row_num]
- return (row[GROUP_NUM], 0 if row[IS_HEADER] else 1,
- l(row[DESCRIPTION_BIG]), l(row[DESCRIPTION_SMALL]))
- row_nums = range(len(self))
- row_nums.sort(key=sort_value)
- self.reorder(row_nums)
-
- def cancel_refresh(self):
- """Asynchronously cancel a `refresh` operation."""
- self._cancel = True
-
- def cancel_download(self):
- """Asynchronously cancel a `download_selected_updates` operation."""
- self._cancel = True
-
- 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_EXISTS] else 0)
-
- def updates_selected(self):
- """Return the number of updates selected."""
- return self._sum_rows(lambda r: 1 if
- r[UPDATE_EXISTS] 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_EXISTS] and r[UPDATE_SELECTED] else 0)
- def unselect_all(self):
- """Unselect all available updates."""
- for row in self:
- if not row[IS_HEADER]:
- row[UPDATE_SELECTED] = False
-
- def select_all(self):
- """Select all available updates."""
- for row in self:
- if not row[IS_HEADER]:
- row[UPDATE_SELECTED] = True
-
- 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
-
- def _invalidate(self):
- """Set the 'is-valid' property to False and notify listeners."""
- if self._is_valid:
- # don't notify if already invalid
- self._is_valid = False
- self.notify('is-valid')
-
- def saw_network_failure(self):
- """Returns true iff there was at least one network failure during the
- last refresh operation."""
- return len(self._network_failures) > 0
-
- def saw_network_success(self):
- """Returns true iff there was at least one successful network
- transaction during the last refresh operation."""
- return self._saw_network_success
-
- def do_get_property(self, prop):
- """Standard interface to access the 'is-valid' property."""
- if prop.name == 'is-valid': return self.is_valid()
- if prop.name == 'saw-network-failure': return self.saw_network_failure()
- if prop.name == 'saw-network-success': return self.saw_network_success()
- raise AttributeError, 'unknown property %s' % prop.name
-
- def add_group(self, group_url):
- """Add the group referenced by the given `group_url` to the end of the
- groups list. This invalidates the UpdateList; you'll have to call
- the refresh method to revalidate."""
- # sanity check
- if not group_url.strip():
- return False # nothing to it
- # double check that group not already present.
- for row in self:
- if row[IS_HEADER] and row[UPDATE_URL] == group_url:
- return False # already there.
- # find the group number we should use for this new group.
- new_gnum = None
- for row in self:
- if row[IS_HEADER] and row[UPDATE_URL] is None:
- new_gnum = row[GROUP_NUM]
- if new_gnum is not None and row[GROUP_NUM] >= new_gnum:
- # renumber to make room.
- row[GROUP_NUM] += 1
- # add the new group!
- self._append(at=0,
- IS_HEADER=True,
- GROUP_NUM = new_gnum,
- DESCRIPTION_BIG=_('New group'),
- DESCRIPTION_SMALL=group_url,
- UPDATE_URL=group_url)
- self._sort()
- self._write_groups()
- # invalid: need to refresh to update activities.
- self._invalidate()
- return True
-
- def del_group(self, group_url):
- """Delete all entries associated with the group identified by the
- given `group_url`. This invalidates the UpdateList; you'll have to
- call the refresh method to revalidate."""
- group_num = None
- # YUCK! Removing a group of rows in a tree model is *ugly*.
- row = self.get_iter_first()
- while row is not None:
- remove = False
- if group_num is None:
- if self[row][IS_HEADER] and self[row][UPDATE_URL] == group_url:
- group_num = self[row][GROUP_NUM]
- remove = True
- elif self[row][GROUP_NUM] == group_num:
- remove = True
- else:
- self[row][GROUP_NUM] -= 1
- if not remove:
- row = self.iter_next(row)
- elif not self.remove(row):
- row = None # removed last row.
- if group_num is None:
- return False # not found.
- self._sort()
- self._write_groups()
- self._invalidate()
- return True
-
- def move_group(self, group_url, desired_num=-1):
- """Move the group with the given UPDATE_URL to the specified location.
- The location is specified *before* the existing row for the group is
- deleted, so the final location may vary. If desired_num is less than
- 0, then the row is moved to the very end."""
- initial_num, max_num = None, None
- # find the row, first.
- for row in self:
- if row[IS_HEADER]:
- max_num = row[GROUP_NUM]
- if row[UPDATE_URL] == group_url:
- initial_num = row[GROUP_NUM]
- if initial_num is None or max_num is None:
- return False # can't find it, or there are no rows.
- # now make a mapping to define the desired reordering.
- if desired_num < 0: desired_num = max_num
- if desired_num == initial_num: return False # no change
- if desired_num > initial_num: desired_num -= 1
- def make_mapping():
- new_nums = list(xrange(max_num))
- del new_nums[initial_num]
- new_nums.insert(desired_num, initial_num)
- new_nums.append(max_num)
- return new_nums
- m = dict(zip(make_mapping(), xrange(max_num+1)))
- # reset the group numbers appropriately
- for row in self:
- row[GROUP_NUM] = m[row[GROUP_NUM]]
- # sort to actually reorder
- self._sort()
- # save these changes
- self._write_groups()
- # not valid because activities may need group reassignment.
- self._invalidate()
- return True
-
- def _write_groups(self):
- """Write a new user groups file based on the current model."""
- with open(actinfo.USER_GROUPS_FILE, 'w') as f:
- for row in self:
- if row[IS_HEADER] and row[UPDATE_URL] is not None:
- print >>f, row[UPDATE_URL]
-
- def download_selected_updates(self, progress_cb=(lambda n, row: None),
- dir=None):
- """Return a generator giving (row, local filename) pairs for
- each selected update. Caller is responsible for unlinking
- these files when they are through. The `progress_cb` gets a
- floating point number in [0, 1] indicating the amount of the
- download which is complete, and a reference to the row in this
- `UpdateList` describing the current download. The files will
- be created in the directory specified by `dir`, if it is
- given, otherwise they will be created in the user's activity
- directory. If the remote HTTP server does not provide information
- about the length of some of the downloads, the first parameter
- to the `progress_cb` will be `None` during those downloads.
- """
- if dir is None: dir=actinfo.USER_ACTIVITY_DIR
- if not os.path.isdir(dir): os.makedirs(dir)
- self._cancel = False
- sizes = [ 0, self.updates_size() ]
- for row in self:
- if self._cancel: return # bail.
- if row[IS_HEADER]: continue
- if not (row[UPDATE_EXISTS] and row[UPDATE_SELECTED]): continue
- def report(cursize, totalsize_):
- if totalsize_ is None:
- progress_cb(None, row)
- else:
- progress_cb((sizes[0]+cursize) / sizes[1], row)
- try:
- yield row, urlrange.urlretrieve(row[UPDATE_URL], dir=dir,
- reporthook=report,
- cancelhook=lambda:self._cancel,
- store_cache=False,
- timeout=HTTP_TIMEOUT)
- except urlrange.CancelException:
- return # bail.
- except (HTMLParseError, IOError, socket.error):
- yield row, None # network error
- # XXX: if rows eventually have 'indeterminate size', then we
- # should update this progress computation, probably based
- # on the size of the file we just yielded above!
- sizes[0] += row[UPDATE_SIZE]
- progress_cb(sizes[0] / sizes[1], row)
-
-
-#########################################################################
-# Control panel keys for command-line and GUI use.
-
-# note that the *presence* of a key is defined by the presence of a
-# get_<key> method, the *documentation* for a key is given by the
-# doc string of the set_<key> method, but actually looking at
-# values from the command-line is performed using print_<key>.
-
-def _print_status(n, extra):
- """Returns a helper function which print a human-readable
- completion percentage."""
- sys.stdout.write(' '*79)
- sys.stdout.write('\r')
- if extra is None: extra=''
- if n is None:
- sys.stdout.write('%s\r' % extra[:77].ljust(77))
- else:
- sys.stdout.write('%s %5.1f%%\r' % (extra[:70].ljust(70), n*100))
- sys.stdout.flush()
-
-# 'available_updates' key -----------------------------
-def get_available_updates():
- # only here to get this key to show up for '-l'
- raise ValueError('This method not used by the GUI')
-
- at inhibit_suspend
-def print_available_updates():
- """Print the set of available updates in nice human-readable fashion."""
- ul = UpdateList(skip_icons=True)
- ul.refresh(_print_status)
- print
- def opt(x):
- if x is None or x == '': return ''
- return ': %s' % x
- for row in ul:
- if not row[UPDATE_EXISTS]: continue # skip
- if row[IS_HEADER]:
- print row[DESCRIPTION_BIG] + opt(row[DESCRIPTION_SMALL])
- else:
- print '*', row[DESCRIPTION_BIG] + opt(row[DESCRIPTION_SMALL])
- print
- print _('%(number)d updates available. Size: %(size)s') % \
- { 'number': ul.updates_available(),
- 'size': _humanize_size(ul.updates_size()) }
-
-def set_available_updates():
- # NOTE slightly odd way to document this control panel key
- """Retrieve the list of available activity updates."""
- raise ValueError(_("Setting the list of updates is not permitted."))
-
-# 'install_updates' key -----------------------------
-def get_install_update():
- # this func is needed to make the 'install_update' key show up for '-l'
- raise ValueError(_("Only the 'set' operation for this key is defined."))
-
- at inhibit_suspend
-def set_install_update(which):
- """
- Install any available updates for the activity identified by the
- given (localized) name or bundle id, or all available updates if
- 'all' is given.
- """
- import os
- ul = UpdateList(skip_icons=True)
- ul.refresh(_print_status)
- def too_many():
- raise ValueError(_('More than one match found for the given activity name or id.'))
- def no_updates():
- raise ValueError(_('The given activity is already up-to-date.'))
- if which != 'all':
- found = False
- for row in ul:
- row[UPDATE_SELECTED] = False
- for row in ul:
- if row[IS_HEADER]: continue
- if which==row[ACTIVITY_ID]:
- if found: too_many()
- found = True
- if not row[UPDATE_EXISTS]: no_updates()
- row[UPDATE_SELECTED] = True
- if not found:
- found_but_uptodate=False
- for row in ul:
- if row[IS_HEADER]: continue
- if which in row[DESCRIPTION_BIG]:
- if not row[UPDATE_EXISTS]: # could be false match
- found_but_uptodate=True
- continue
- if found: too_many()
- found = True
- row[UPDATE_SELECTED] = True
- if not found:
- if found_but_uptodate: no_updates()
- raise ValueError(_('No activity found with the given name or id.'))
- assert ul.updates_selected() == 1
- # okay, now we've selected only our desired updates. Download and
- # install them!
- # we need to set up a glib event loop in order to connect to the
- # activity registry (sigh) in ActivityBundle.upgrade() below.
- from dbus.mainloop.glib import DBusGMainLoop
- DBusGMainLoop(set_as_default=True)
- # we'll fetch the main loop, but we don't actually have to run it.
- loop = gobject.MainLoop()
- from sugar.activity.registry import get_registry
- registry = get_registry()
- def reporthook(n, row):
- _print_status(n, _('Downloading %s...') % row[DESCRIPTION_BIG])
- for row, f in ul.download_selected_updates(reporthook):
- if f is None: continue # cancelled or network error
- try:
- _print_status(None, _('Examining %s...') % row[DESCRIPTION_BIG])
- b = actutils.BundleHelper(f)
- if b.is_installed():
- _print_status(None, _('Upgrading %s...') % row[DESCRIPTION_BIG])
- else:
- _print_status(None, _('Installing %s...')% row[DESCRIPTION_BIG])
- b.install_or_upgrade(registry)
- except:
- print
- print _('Error installing %s.') % row[DESCRIPTION_BIG]
- import traceback
- traceback.print_exc() # complain! but go on.
- if os.path.exists(f):
- os.unlink(f)
- else:
- print "Failed trying to clean up", f
-
-# 'update_groups' key -----------------------------
-def get_update_groups():
- # only here to get this key to show up for '-l'
- raise ValueError('This method not used by the GUI')
-
-def print_update_groups():
- group_urls = actinfo.get_activity_group_urls()
- for gurl in group_urls:
- # we could do a network operation to get a prettier name for
- # the group here, but let's keep the cmd-line interface simple.
- print gurl
-
-def set_update_groups(groups):
- """
- Set URLs for update groups used by the software update control panel.
- Activities referenced by the given URLs will be suggested for installation
- on this laptop, if they are not already present. This allows deployments
- to update the set of activities installed, as well as the actual activities.
-
- The parameter should be a white-space separated list of group URLs.
- """
- with open(actinfo.USER_GROUPS_FILE, 'w') as f:
- for gurl in groups.split():
- print >>f, gurl
-
-#########################################################################
-# Self-test code.
-def _main():
- """Self-test."""
- print_available_updates()
- #set_install_update('all')
-
-if __name__ == '__main__': _main ()
diff --git a/setup.py b/setup.py
index ebdace0..f686c00 100644
--- a/setup.py
+++ b/setup.py
@@ -7,10 +7,8 @@ setup(name='sugar-update-control',
author_email='cscott at laptop.org',
url='http://dev.laptop.org/git/users/cscott/sugar-update-control',
packages=['bitfrost','bitfrost.update','bitfrost.util'],
- data_files=[('/usr/share/sugar/shell/controlpanel/model',
- ['model/updater.py']),
- ('/usr/share/sugar/shell/controlpanel/view',
- ['view/updater.py']),
+ data_files=[('/usr/share/sugar/extensions/cpsection/updater',
+ ['src/__init__.py', 'src/model.py', 'src/view.py']),
('/usr/share/sugar/data/icons',
['module-updater.svg']),
],
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..a509dfe
--- /dev/null
+++ b/src/__init__.py
@@ -0,0 +1,23 @@
+# 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/src/model.py b/src/model.py
new file mode 100755
index 0000000..1461c62
--- /dev/null
+++ b/src/model.py
@@ -0,0 +1,861 @@
+#!/usr/bin/python2.5
+# Copyright (C) 2008 One Laptop Per Child Association, Inc.
+# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
+# Written by C. Scott Ananian <cscott at laptop.org>
+"""Activity updater: backing model.
+
+This module implements the non-GUI portions of the activity updater,
+including in particular the master list of groups, activities, whether
+updates are needed, and the URL at which to find the updated activity.
+
+Because `UpdateList` inherits from `gtk.ListStore` so that it plays
+nicely with a GUI, this module requires `gtk`. Those of you without
+DISPLAY set will have to put up with a GtkWarning that the display
+couldn't be opened. Sorry.
+"""
+from __future__ import with_statement
+from __future__ import division
+
+# for testing
+_DEBUG_MAKE_ALL_OLD = False
+_DEBUG_CHECK_VERSIONS = False
+
+# default timeout for HTTP connections, in seconds
+HTTP_TIMEOUT=30
+
+import gtk
+import gobject
+
+import locale
+import os
+import os.path
+import socket
+import sys
+import traceback
+import zipfile
+from gettext import gettext as _
+from HTMLParser import HTMLParseError
+from urllib2 import HTTPError
+
+import bitfrost.update.actinfo as actinfo
+import bitfrost.update.actutils as actutils
+import bitfrost.update.microformat as microformat
+import bitfrost.util.urlrange as urlrange
+
+# weak dependency on inhibit_suspend from olpc-update package
+try:
+ from bitfrost.update import inhibit_suspend
+except ImportError:
+ # use a no-op substitude for the inhibit_suspend decorator.
+ inhibit_suspend = lambda f: f
+
+# lifted from gnome-update-manager/Common/utils
+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 _svg2pixbuf(icon_data):
+ """Convert the given `icon_data` SVG string to a `gtk.gdk.Pixbuf`
+ with maximum size 55x55."""
+ import re, rsvg
+ # substitute black/white for icon color entities.
+ for entity, value in [('stroke_color','#808080'),
+ ('fill_color','#eee')]:
+ xml = '<!ENTITY %s "%s">' % (entity, value)
+ icon_data = re.sub('<!ENTITY %s .*>' % entity, xml, icon_data)
+ h = rsvg.Handle(data=icon_data)
+ if h.get_property('width') > 55 or h.get_property('height') > 55:
+ # lame! scale it.
+ print "WARNING: oversize icon (%dx%d), scaling." % \
+ (h.get_property('width'), h.get_property('height'))
+ del h
+ pbl = gtk.gdk.PixbufLoader()
+ pbl.set_size(55, 55)
+ pbl.write(icon_data)
+ pbl.close()
+ return pbl.get_pixbuf()
+ return h.get_pixbuf()
+
+_parse_cache = {}
+def _check_for_updates(url, activity_id):
+ """Downloads the given URL, parses it (caching the result), and returns
+ a list of (version, url) pairs present for the given `activity_id`.
+ Returns `None` if there was a problem downloading the URL.
+ Returns a zero-length list if the given URL is unparsable or does not
+ contain information for the desired activity_id."""
+ global _parse_cache
+ if url not in _parse_cache:
+ try:
+ __, __, _parse_cache[url] = \
+ microformat.parse_url(url, timeout=HTTP_TIMEOUT)
+ if _DEBUG_CHECK_VERSIONS:
+ # for kicks and giggles, verify these version #s!
+ for n_activity_id, versions in _parse_cache[url].items():
+ for ver, url2 in versions:
+ actual_id, actual_ver = \
+ actutils.id_and_version_from_url(url2)
+ if actual_id != n_activity_id:
+ print "ACTIVITY ID SHOULD BE", n_activity_id, \
+ "BUT ACTUALLY IS", actual_id, ("(%s)"%url2)
+ if actual_ver != ver:
+ print "VERSION SHOULD BE", ver, \
+ "BUT ACTUALLY IS", actual_ver, ("(%s)"%url2)
+ except HTMLParseError:
+ _parse_cache[url] = {} # parse error
+ except (IOError, socket.error):
+ _parse_cache[url] = None # network error
+ activity_map = _parse_cache[url]
+ if activity_map is None: return None # error attempting to check.
+ if activity_id not in activity_map: return [] # no versions found.
+ return activity_map[activity_id]
+
+def _retrieve_update_version(actbun,
+ network_success_cb=(lambda url: None),
+ network_failure_cb=(lambda url: None)):
+ """Return a tuple of current version, new version, url for new
+ version, size of new version, given a current activity bundle.
+ All the information about the new version is `None` if no newer
+ update can be found. The optional `network_success_cb` is invoked
+ with a single `url` parameter whenever a network operation
+ succeeds, and the optional `network_failure_cb` is invoked with a
+ single `url` parameter if all fallback network operations for a
+ given activity bundle fail."""
+ update_url = actbun.get_update_url()
+ oldv = 0 if _DEBUG_MAKE_ALL_OLD else actbun.get_activity_version()
+ # here we implement the search for a release-specific version
+ # XXX: would be nice to use retrieve_first_variant here
+ vlist = None
+ for uu in actinfo.url_variants(update_url):
+ vlist = _check_for_updates(uu, actbun.get_bundle_id())
+ if vlist is not None:
+ network_success_cb(uu)
+ if len(vlist) > 0:
+ break # found some info
+ if vlist is None:
+ # all url variants for the given actbun have failed.
+ network_failure_cb(update_url)
+ return oldv, None, None, 0
+ newv, newu = microformat.only_best_update(vlist + [(oldv, None)])
+ if newu is None or newv==oldv: return oldv, None, None, 0 # no updates
+ # find size of new version.
+ try:
+ size = urlrange.urlopen(newu, timeout=HTTP_TIMEOUT).length()
+ return oldv, newv, newu, size
+ except (HTTPError, IOError, socket.error):
+ # hmm, i guess that url isn't valid after all. bail.
+ return oldv, None, None, 0 # there are no *actual* updates.
+
+##########################################################################
+# Fundamental data object: the real work gets done here.
+
+_column_name_map = dict(globals())
+ACTIVITY_ID, ACTIVITY_BUNDLE, ACTIVITY_ICON, \
+ UPDATE_URL, UPDATE_VERSION, UPDATE_SIZE, \
+ UPDATE_EXISTS, UPDATE_SELECTED, \
+ DESCRIPTION_BIG, DESCRIPTION_SMALL, IS_HEADER, GROUP_NUM = \
+ xrange(12)
+"""List of columns in the `UpdateList`."""
+_column_name_map = dict((k,v) for k,v in globals().items()
+ if k not in _column_name_map and k!='_column_name_map')
+"""Mapping from column names to indices."""
+
+class UpdateList(gtk.ListStore):
+ """Model which provides backing storage for the activity 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, skip_icons=False):
+ gtk.ListStore.__init__(self,
+ # column types
+ str, object, gtk.gdk.Pixbuf,
+ str, long, long,
+ bool, bool, str, str, bool, int)
+ self._skip_icons = skip_icons
+ self._is_valid = False
+ self._network_failures = []
+ self._saw_network_success = False
+
+ def __del__(self):
+ """Free up any memory held by the cache in urlrange."""
+ urlrange.urlcleanup()
+
+ def _append(self, at=None, **kwargs):
+ """Utility function to make it easier to add rows and get paths."""
+ global _column_name_map
+ # defaults for each column
+ row = [None, None, None,
+ None, 0, 0,
+ True, True, None, None, False, 0]
+ # set entries in the row based on kwargs
+ for k,v in kwargs.items():
+ row[_column_name_map[k]] = v
+ if at is not None:
+ it = self.insert(at, row)
+ else:
+ it = self.append(row)
+ return self.get_path(it)
+
+ def toggle_select(self, path):
+ """Toggle whether the given update will be installed."""
+ row = self[path]
+ row[UPDATE_SELECTED] = not row[UPDATE_SELECTED]
+
+ # don't touch the UI in refresh, it needs to be thread-safe.
+ # (model will be disconnected from the view before invoking refresh
+ # in another thread)
+ def refresh(self, progress_callback=lambda n, extra: None,
+ clear_cache=True):
+ """Perform network operations to find available updates.
+
+ The `progress_callback` is invoked with numbers between 0 and 1
+ or `None` as the network queries complete. The last callback will be
+ `progress_callback(1)`. Passing `None` to `progress_callback`
+ requests "pulse mode" from the progress bar.
+ """
+ global _parse_cache
+ if clear_cache:
+ _parse_cache = {} # clear microformat parse cache
+ urlrange.urlcleanup() # clean url cache
+ self._cancel = False
+ self._invalidate()
+ # don't notify for the following; we'll notify at the end when we
+ # know what the new values ought to be.
+ self._saw_network_success = False
+ self._network_failures = []
+ # bookkeeping
+ progress_callback(None, None)
+ self.clear()
+ # find all activities already installed.
+ progress_callback(None, _('Looking for local activities and content...')) # pulse
+ activities = actinfo.get_activities() + actinfo.get_libraries()
+ # enumerate all group urls
+ progress_callback(None, _('Loading groups...'))
+ group_urls = actinfo.get_activity_group_urls()
+ # now we've got enough information to allow us to compute a
+ # reasonable completion percentage.
+ steps_total = [ len(activities) + len(group_urls) + 3 ]
+ steps_count = [ 0 ] # box this to allow update from mkprog.
+ def mkprog(msg=None):
+ """Helper function to do progress update."""
+ steps_count[0] += 1
+ progress_callback(steps_count[0]/steps_total[0], msg)
+ mkprog(_('Loading groups...'))
+ # okay, first load up any group definitions; these take precedence
+ # if present.
+ groups = []
+ def group_parser(f, url):
+ name, desc, groups = microformat.parse_html(f.read(), url)
+ if len(groups) > 0 or (name is not None and desc is not None):
+ return name, desc, groups
+ return None # hmm, not a successful parse.
+ for gurl in group_urls:
+ mkprog(_('Fetching %s...') % gurl)
+ if self._cancel: break # bail!
+ gdata = actinfo.retrieve_first_variant(gurl, group_parser,
+ timeout=HTTP_TIMEOUT)
+ if gdata is not None:
+ gname, gdesc, gactmap = gdata
+ groups.append((gname, gdesc, gurl, gactmap))
+ self._saw_network_success = True
+ else:
+ # headers even for failed groups.
+ groups.append((None, gurl, gurl, {}))
+ self._network_failures.append(gurl)
+ # now start filling up the liststore, keeping a map from activity id
+ # to liststore path
+ row_map = {}
+ group_num = 0
+ for gname, gdesc, gurl, gactmap in groups:
+ # add group header.
+ if gname is None: gname = _('Activity Group')
+ self._append(IS_HEADER=True,
+ UPDATE_URL=gurl,
+ GROUP_NUM=group_num,
+ DESCRIPTION_BIG=gname,
+ DESCRIPTION_SMALL=gdesc)
+ # now add entries for all activities in the group, whether
+ # currently installed or not.
+ for act_id, version_list in sorted(gactmap.items()):
+ version, url = microformat.only_best_update(version_list)
+ if act_id not in row_map:
+ # temporary description in case user cancels the refresh
+ tmp_desc = act_id.replace('sugar-is-lame',
+ 'lame-is-the-new-cool')
+ row_map[act_id] = self._append(ACTIVITY_ID=act_id,
+ GROUP_NUM=group_num,
+ UPDATE_EXISTS=True,
+ UPDATE_URL=url,
+ UPDATE_VERSION=version,
+ DESCRIPTION_BIG=tmp_desc)
+ steps_total[0] += 1 # new activity?
+ else:
+ # allow for a later version in a different group
+ row = self[row_map[act_id]]
+ if version > row[UPDATE_VERSION]:
+ row[UPDATE_URL] = url
+ # XXX: deal with pinned updates.
+ group_num += 1
+ # add in information from local activities.
+ self._append(IS_HEADER=True, GROUP_NUM=group_num,
+ DESCRIPTION_BIG=_('Local activities'))
+ for act in activities:
+ act_id = act.get_bundle_id()
+ if act_id not in row_map:
+ row_map[act_id] = self._append(ACTIVITY_ID=act_id,
+ GROUP_NUM=group_num,
+ UPDATE_EXISTS=False)
+ else:
+ steps_total[0] -= 1 # correct double-counting.
+ # update icon, and bundle
+ row = self[row_map[act_id]]
+ row[ACTIVITY_BUNDLE] = act
+ row[DESCRIPTION_BIG] = act.get_name()
+ if not self._skip_icons:
+ try:
+ row[ACTIVITY_ICON] = _svg2pixbuf(act.get_icon_data())
+ except IOError:
+ # dlo trac #8149: don't kill updater if existing icon
+ # bundle is malformed.
+ pass
+ group_num += 1
+ # now do extra network traffic to look for actual updates.
+ def refresh_existing(row):
+ """Look for updates to an existing activity."""
+ act = row[ACTIVITY_BUNDLE]
+ def net_good(url_): self._saw_network_success = True
+ def net_bad(url): self._network_failures.append(url)
+ oldver, newver, newurl, size = \
+ _retrieve_update_version(act, net_good, net_bad)
+ # merge w/ info from the activity group
+ # (activity group entries have UPDATE_EXISTS=True)
+ if row[UPDATE_EXISTS] and \
+ ((act.get_activity_version() if newver is None else newver) \
+ < row[UPDATE_VERSION]):
+ newver, newurl = row[UPDATE_VERSION], row[UPDATE_URL]
+ size = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)\
+ .length()
+ row[UPDATE_EXISTS] = (newver is not None)
+ row[UPDATE_URL] = newurl
+ row[UPDATE_SIZE] = size
+ if newver is None:
+ description = _('At version %s') % oldver
+ else:
+ description = \
+ _('From version %(old)d to %(new)d (Size: %(size)s)') % \
+ { 'old':oldver, 'new':newver, 'size':_humanize_size(size) }
+ row[UPDATE_SELECTED] = True
+ row[DESCRIPTION_SMALL] = description
+ def refresh_new(row):
+ """Look for updates to a new activity in the group."""
+ uo = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)
+ row[UPDATE_SIZE] = uo.length()
+ zf = zipfile.ZipFile(uo)
+ # grab data from activity.info file
+ activity_base = actutils.bundle_base_from_zipfile(zf)
+ try:
+ zf.getinfo('%s/activity/activity.info' % activity_base)
+ is_activity = True
+ except KeyError:
+ is_activity = False
+ if is_activity:
+ cp = actutils.activity_info_from_zipfile(zf)
+ SECTION = 'Activity'
+ else:
+ cp = actutils.library_info_from_zipfile(zf)
+ SECTION = 'Library'
+ act_id = None
+ for fieldname in ('bundle_id', 'service_name', 'global_name'):
+ if cp.has_option(SECTION, fieldname):
+ act_id = cp.get(SECTION, fieldname)
+ break
+ if not act_id:
+ raise RuntimeError("bundle_id not found for %s" %
+ row[UPDATE_URL])
+ name = act_id
+ if cp.has_option(SECTION, 'name'):
+ name = cp.get(SECTION, 'name')
+ # okay, try to get an appropriately translated name.
+ if is_activity:
+ lcp = actutils.locale_activity_info_from_zipfile(zf)
+ if lcp is not None:
+ name = lcp.get(SECTION, 'name')
+ else:
+ s = actutils.locale_section_for_content_bundle(cp)
+ if s is not None and cp.has_option(s, 'name'):
+ name = cp.get(s, 'name')
+ version = None
+ for fieldname in ('activity_version', 'library_version'):
+ if cp.has_option(SECTION, fieldname):
+ version = int(cp.get(SECTION, fieldname))
+ break
+ if version is None:
+ raise RuntimeError("can't find version for %s" %
+ row[UPDATE_URL])
+ row[DESCRIPTION_BIG] = name
+ row[DESCRIPTION_SMALL] = \
+ _('New version %(version)s (Size: %(size)s)') % \
+ {'version':version, 'size':_humanize_size(row[UPDATE_SIZE])}
+ # okay, let's try to update the icon!
+ if not self._skip_icons:
+ if is_activity:
+ # XXX should failures here kill the upgrade?
+ icon_file = cp.get(SECTION, 'icon')
+ icon_filename = '%s/activity/%s.svg'%(activity_base, icon_file)
+ row[ACTIVITY_ICON] = _svg2pixbuf(zf.read(icon_filename))
+ else:
+ row[ACTIVITY_ICON] = _svg2pixbuf(actinfo.DEFAULT_LIBRARY_ICON)
+ # go through activities and do network traffic
+ for row in self:
+ if self._cancel: break # bail!
+ if row[IS_HEADER]: continue # skip
+ mkprog(_('Checking %s...') % row[DESCRIPTION_BIG])
+ try:
+ if row[ACTIVITY_BUNDLE] is None:
+ refresh_new(row)
+ self._saw_network_success = True
+ else:
+ refresh_existing(row)
+ except:
+ row[UPDATE_EXISTS] = False # something wrong, can't update
+ if row[UPDATE_URL] is not None:
+ self._network_failures.append(row[UPDATE_URL])
+ # log the problem for later debugging.
+ print "Failure updating", row[DESCRIPTION_BIG], \
+ row[DESCRIPTION_SMALL], row[UPDATE_URL]
+ traceback.print_exc()
+ mkprog('Sorting...') # all done
+ # hide headers if all children are hidden
+ sawone, last_header = False, None
+ for row in self:
+ if row[IS_HEADER]:
+ if last_header is not None:
+ last_header[UPDATE_EXISTS] = sawone
+ sawone, last_header = False, row
+ elif row[UPDATE_EXISTS]:
+ sawone = True
+ if last_header is not None:
+ last_header[UPDATE_EXISTS] = sawone
+ # finally, sort all rows.
+ self._sort()
+ mkprog() # all done
+ # XXX: check for base os update, and add an entry here?
+ self._is_valid = True
+ self.notify('is-valid')
+ self.notify('saw-network-failure')
+ self.notify('saw-network-success')
+
+ def _sort(self):
+ """Sort rows by group number, then case-insensitively by description."""
+ l = lambda s: s.decode('utf-8','ignore').lower() \
+ if s is not None else None
+ def sort_value(row_num):
+ row = self[row_num]
+ return (row[GROUP_NUM], 0 if row[IS_HEADER] else 1,
+ l(row[DESCRIPTION_BIG]), l(row[DESCRIPTION_SMALL]))
+ row_nums = range(len(self))
+ row_nums.sort(key=sort_value)
+ self.reorder(row_nums)
+
+ def cancel_refresh(self):
+ """Asynchronously cancel a `refresh` operation."""
+ self._cancel = True
+
+ def cancel_download(self):
+ """Asynchronously cancel a `download_selected_updates` operation."""
+ self._cancel = True
+
+ 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_EXISTS] else 0)
+
+ def updates_selected(self):
+ """Return the number of updates selected."""
+ return self._sum_rows(lambda r: 1 if
+ r[UPDATE_EXISTS] 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_EXISTS] and r[UPDATE_SELECTED] else 0)
+ def unselect_all(self):
+ """Unselect all available updates."""
+ for row in self:
+ if not row[IS_HEADER]:
+ row[UPDATE_SELECTED] = False
+
+ def select_all(self):
+ """Select all available updates."""
+ for row in self:
+ if not row[IS_HEADER]:
+ row[UPDATE_SELECTED] = True
+
+ 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
+
+ def _invalidate(self):
+ """Set the 'is-valid' property to False and notify listeners."""
+ if self._is_valid:
+ # don't notify if already invalid
+ self._is_valid = False
+ self.notify('is-valid')
+
+ def saw_network_failure(self):
+ """Returns true iff there was at least one network failure during the
+ last refresh operation."""
+ return len(self._network_failures) > 0
+
+ def saw_network_success(self):
+ """Returns true iff there was at least one successful network
+ transaction during the last refresh operation."""
+ return self._saw_network_success
+
+ def do_get_property(self, prop):
+ """Standard interface to access the 'is-valid' property."""
+ if prop.name == 'is-valid': return self.is_valid()
+ if prop.name == 'saw-network-failure': return self.saw_network_failure()
+ if prop.name == 'saw-network-success': return self.saw_network_success()
+ raise AttributeError, 'unknown property %s' % prop.name
+
+ def add_group(self, group_url):
+ """Add the group referenced by the given `group_url` to the end of the
+ groups list. This invalidates the UpdateList; you'll have to call
+ the refresh method to revalidate."""
+ # sanity check
+ if not group_url.strip():
+ return False # nothing to it
+ # double check that group not already present.
+ for row in self:
+ if row[IS_HEADER] and row[UPDATE_URL] == group_url:
+ return False # already there.
+ # find the group number we should use for this new group.
+ new_gnum = None
+ for row in self:
+ if row[IS_HEADER] and row[UPDATE_URL] is None:
+ new_gnum = row[GROUP_NUM]
+ if new_gnum is not None and row[GROUP_NUM] >= new_gnum:
+ # renumber to make room.
+ row[GROUP_NUM] += 1
+ # add the new group!
+ self._append(at=0,
+ IS_HEADER=True,
+ GROUP_NUM = new_gnum,
+ DESCRIPTION_BIG=_('New group'),
+ DESCRIPTION_SMALL=group_url,
+ UPDATE_URL=group_url)
+ self._sort()
+ self._write_groups()
+ # invalid: need to refresh to update activities.
+ self._invalidate()
+ return True
+
+ def del_group(self, group_url):
+ """Delete all entries associated with the group identified by the
+ given `group_url`. This invalidates the UpdateList; you'll have to
+ call the refresh method to revalidate."""
+ group_num = None
+ # YUCK! Removing a group of rows in a tree model is *ugly*.
+ row = self.get_iter_first()
+ while row is not None:
+ remove = False
+ if group_num is None:
+ if self[row][IS_HEADER] and self[row][UPDATE_URL] == group_url:
+ group_num = self[row][GROUP_NUM]
+ remove = True
+ elif self[row][GROUP_NUM] == group_num:
+ remove = True
+ else:
+ self[row][GROUP_NUM] -= 1
+ if not remove:
+ row = self.iter_next(row)
+ elif not self.remove(row):
+ row = None # removed last row.
+ if group_num is None:
+ return False # not found.
+ self._sort()
+ self._write_groups()
+ self._invalidate()
+ return True
+
+ def move_group(self, group_url, desired_num=-1):
+ """Move the group with the given UPDATE_URL to the specified location.
+ The location is specified *before* the existing row for the group is
+ deleted, so the final location may vary. If desired_num is less than
+ 0, then the row is moved to the very end."""
+ initial_num, max_num = None, None
+ # find the row, first.
+ for row in self:
+ if row[IS_HEADER]:
+ max_num = row[GROUP_NUM]
+ if row[UPDATE_URL] == group_url:
+ initial_num = row[GROUP_NUM]
+ if initial_num is None or max_num is None:
+ return False # can't find it, or there are no rows.
+ # now make a mapping to define the desired reordering.
+ if desired_num < 0: desired_num = max_num
+ if desired_num == initial_num: return False # no change
+ if desired_num > initial_num: desired_num -= 1
+ def make_mapping():
+ new_nums = list(xrange(max_num))
+ del new_nums[initial_num]
+ new_nums.insert(desired_num, initial_num)
+ new_nums.append(max_num)
+ return new_nums
+ m = dict(zip(make_mapping(), xrange(max_num+1)))
+ # reset the group numbers appropriately
+ for row in self:
+ row[GROUP_NUM] = m[row[GROUP_NUM]]
+ # sort to actually reorder
+ self._sort()
+ # save these changes
+ self._write_groups()
+ # not valid because activities may need group reassignment.
+ self._invalidate()
+ return True
+
+ def _write_groups(self):
+ """Write a new user groups file based on the current model."""
+ with open(actinfo.USER_GROUPS_FILE, 'w') as f:
+ for row in self:
+ if row[IS_HEADER] and row[UPDATE_URL] is not None:
+ print >>f, row[UPDATE_URL]
+
+ def download_selected_updates(self, progress_cb=(lambda n, row: None),
+ dir=None):
+ """Return a generator giving (row, local filename) pairs for
+ each selected update. Caller is responsible for unlinking
+ these files when they are through. The `progress_cb` gets a
+ floating point number in [0, 1] indicating the amount of the
+ download which is complete, and a reference to the row in this
+ `UpdateList` describing the current download. The files will
+ be created in the directory specified by `dir`, if it is
+ given, otherwise they will be created in the user's activity
+ directory. If the remote HTTP server does not provide information
+ about the length of some of the downloads, the first parameter
+ to the `progress_cb` will be `None` during those downloads.
+ """
+ if dir is None: dir=actinfo.USER_ACTIVITY_DIR
+ if not os.path.isdir(dir): os.makedirs(dir)
+ self._cancel = False
+ sizes = [ 0, self.updates_size() ]
+ for row in self:
+ if self._cancel: return # bail.
+ if row[IS_HEADER]: continue
+ if not (row[UPDATE_EXISTS] and row[UPDATE_SELECTED]): continue
+ def report(cursize, totalsize_):
+ if totalsize_ is None:
+ progress_cb(None, row)
+ else:
+ progress_cb((sizes[0]+cursize) / sizes[1], row)
+ try:
+ yield row, urlrange.urlretrieve(row[UPDATE_URL], dir=dir,
+ reporthook=report,
+ cancelhook=lambda:self._cancel,
+ store_cache=False,
+ timeout=HTTP_TIMEOUT)
+ except urlrange.CancelException:
+ return # bail.
+ except (HTMLParseError, IOError, socket.error):
+ yield row, None # network error
+ # XXX: if rows eventually have 'indeterminate size', then we
+ # should update this progress computation, probably based
+ # on the size of the file we just yielded above!
+ sizes[0] += row[UPDATE_SIZE]
+ progress_cb(sizes[0] / sizes[1], row)
+
+
+#########################################################################
+# Control panel keys for command-line and GUI use.
+
+# note that the *presence* of a key is defined by the presence of a
+# get_<key> method, the *documentation* for a key is given by the
+# doc string of the set_<key> method, but actually looking at
+# values from the command-line is performed using print_<key>.
+
+def _print_status(n, extra):
+ """Returns a helper function which print a human-readable
+ completion percentage."""
+ sys.stdout.write(' '*79)
+ sys.stdout.write('\r')
+ if extra is None: extra=''
+ if n is None:
+ sys.stdout.write('%s\r' % extra[:77].ljust(77))
+ else:
+ sys.stdout.write('%s %5.1f%%\r' % (extra[:70].ljust(70), n*100))
+ sys.stdout.flush()
+
+# 'available_updates' key -----------------------------
+def get_available_updates():
+ # only here to get this key to show up for '-l'
+ raise ValueError('This method not used by the GUI')
+
+ at inhibit_suspend
+def print_available_updates():
+ """Print the set of available updates in nice human-readable fashion."""
+ ul = UpdateList(skip_icons=True)
+ ul.refresh(_print_status)
+ print
+ def opt(x):
+ if x is None or x == '': return ''
+ return ': %s' % x
+ for row in ul:
+ if not row[UPDATE_EXISTS]: continue # skip
+ if row[IS_HEADER]:
+ print row[DESCRIPTION_BIG] + opt(row[DESCRIPTION_SMALL])
+ else:
+ print '*', row[DESCRIPTION_BIG] + opt(row[DESCRIPTION_SMALL])
+ print
+ print _('%(number)d updates available. Size: %(size)s') % \
+ { 'number': ul.updates_available(),
+ 'size': _humanize_size(ul.updates_size()) }
+
+def set_available_updates():
+ # NOTE slightly odd way to document this control panel key
+ """Retrieve the list of available activity updates."""
+ raise ValueError(_("Setting the list of updates is not permitted."))
+
+# 'install_updates' key -----------------------------
+def get_install_update():
+ # this func is needed to make the 'install_update' key show up for '-l'
+ raise ValueError(_("Only the 'set' operation for this key is defined."))
+
+ at inhibit_suspend
+def set_install_update(which):
+ """
+ Install any available updates for the activity identified by the
+ given (localized) name or bundle id, or all available updates if
+ 'all' is given.
+ """
+ import os
+ ul = UpdateList(skip_icons=True)
+ ul.refresh(_print_status)
+ def too_many():
+ raise ValueError(_('More than one match found for the given activity name or id.'))
+ def no_updates():
+ raise ValueError(_('The given activity is already up-to-date.'))
+ if which != 'all':
+ found = False
+ for row in ul:
+ row[UPDATE_SELECTED] = False
+ for row in ul:
+ if row[IS_HEADER]: continue
+ if which==row[ACTIVITY_ID]:
+ if found: too_many()
+ found = True
+ if not row[UPDATE_EXISTS]: no_updates()
+ row[UPDATE_SELECTED] = True
+ if not found:
+ found_but_uptodate=False
+ for row in ul:
+ if row[IS_HEADER]: continue
+ if which in row[DESCRIPTION_BIG]:
+ if not row[UPDATE_EXISTS]: # could be false match
+ found_but_uptodate=True
+ continue
+ if found: too_many()
+ found = True
+ row[UPDATE_SELECTED] = True
+ if not found:
+ if found_but_uptodate: no_updates()
+ raise ValueError(_('No activity found with the given name or id.'))
+ assert ul.updates_selected() == 1
+ # okay, now we've selected only our desired updates. Download and
+ # install them!
+ # we need to set up a glib event loop in order to connect to the
+ # activity registry (sigh) in ActivityBundle.upgrade() below.
+ from dbus.mainloop.glib import DBusGMainLoop
+ DBusGMainLoop(set_as_default=True)
+ # we'll fetch the main loop, but we don't actually have to run it.
+ loop = gobject.MainLoop()
+ from jarabe.model.bundleregistry import get_registry
+ registry = get_registry()
+ def reporthook(n, row):
+ _print_status(n, _('Downloading %s...') % row[DESCRIPTION_BIG])
+ for row, f in ul.download_selected_updates(reporthook):
+ if f is None: continue # cancelled or network error
+ try:
+ _print_status(None, _('Examining %s...') % row[DESCRIPTION_BIG])
+ b = actutils.BundleHelper(f)
+ if b.is_installed(registry):
+ _print_status(None, _('Upgrading %s...') % row[DESCRIPTION_BIG])
+ else:
+ _print_status(None, _('Installing %s...')% row[DESCRIPTION_BIG])
+ b.install_or_upgrade(registry)
+ except:
+ print
+ print _('Error installing %s.') % row[DESCRIPTION_BIG]
+ import traceback
+ traceback.print_exc() # complain! but go on.
+ if os.path.exists(f):
+ os.unlink(f)
+ else:
+ print "Failed trying to clean up", f
+
+# 'update_groups' key -----------------------------
+def get_update_groups():
+ # only here to get this key to show up for '-l'
+ raise ValueError('This method not used by the GUI')
+
+def print_update_groups():
+ group_urls = actinfo.get_activity_group_urls()
+ for gurl in group_urls:
+ # we could do a network operation to get a prettier name for
+ # the group here, but let's keep the cmd-line interface simple.
+ print gurl
+
+def set_update_groups(groups):
+ """
+ Set URLs for update groups used by the software update control panel.
+ Activities referenced by the given URLs will be suggested for installation
+ on this laptop, if they are not already present. This allows deployments
+ to update the set of activities installed, as well as the actual activities.
+
+ The parameter should be a white-space separated list of group URLs.
+ """
+ with open(actinfo.USER_GROUPS_FILE, 'w') as f:
+ for gurl in groups.split():
+ print >>f, gurl
+
+#########################################################################
+# Self-test code.
+def _main():
+ """Self-test."""
+ print_available_updates()
+ #set_install_update('all')
+
+if __name__ == '__main__': _main ()
diff --git a/src/view.py b/src/view.py
new file mode 100755
index 0000000..6cb65ce
--- /dev/null
+++ b/src/view.py
@@ -0,0 +1,688 @@
+#!/usr/bin/python2.5
+# Copyright (C) 2008 One Laptop Per Child Association, Inc.
+# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
+# Written by C. Scott Ananian <cscott at laptop.org>
+"""Activity updater.
+
+Checks for updates to activities and installs them."""
+from __future__ import with_statement
+from __future__ import division
+import pygtk
+pygtk.require('2.0')
+import gtk, gobject
+gtk.gdk.threads_init()
+
+import gettext
+import os
+import re
+from threading import Thread
+from gettext import gettext as _
+
+import bitfrost.update.actutils as actutils
+from sugar.graphics import style
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+import model
+from model import _humanize_size, _svg2pixbuf, inhibit_suspend
+
+# COMPATIBILITY HACK: work around trac #8532 by forcibly removing the
+# SIGCHLD handler (and the necessity for one)
+import sugar.activity.activityfactory
+if hasattr(sugar.activity.activityfactory, '_sigchild_handler'):
+ from ctypes import CDLL, Structure, c_voidp, c_int, c_ulong, pointer
+ import signal
+ libc = CDLL("libc.so.6")
+ SA_NOCLDWAIT = 2 # according to /usr/include/bits/sigaction.h
+ class SIGACTION(Structure):
+ _fields_ = [('sa_handler', c_voidp),
+ ('sa_sigaction', c_voidp),
+ ('sa_mask', c_ulong), # sigset_t
+ ('sa_flags', c_int),
+ ('sa_restorer', c_voidp)]
+ desired = SIGACTION(None, # SIG_DFL, according to /usr/include/asm-generic/signal.h
+ None,
+ 0,
+ SA_NOCLDWAIT,
+ None)
+ libc.sigaction(signal.SIGCHLD, pointer(desired), None)
+# END COMPATIBILITY HACK
+
+
+# configuration constants needed for control panel framework
+CLASS = 'ActivityUpdater'
+ICON = 'module-updater'
+TITLE = _('Software update')
+
+# for debugging
+_DEBUG_VIEW_ALL=False
+"""View even activities with no pending updates."""
+
+def _escape_markup(s):
+ """Escape special characters in `s` so that it is safe to use in
+ Pango markup. Equivalent to `g_markup_escape_text` in glib."""
+ entities = { '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''', }
+ if s is None: return None
+ return re.sub("[&<>\"']", lambda m: entities[m.group(0)],
+ s.decode('utf-8','ignore')).encode('utf-8')
+_e = _escape_markup
+"""Useful abbreviation."""
+
+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
+
+### Pieces of the activity updater view; factored to make the UI structure
+### more apparent in `ActivityUpdater.__init__`.
+
+class ActivityListView(gtk.ScrolledWindow):
+ """List view at the top, showing activities, versions, and sizes."""
+ def __init__(self, activity_updater, activity_pane):
+ gtk.ScrolledWindow.__init__(self)
+ self.activity_updater = activity_updater
+ self.activity_pane = activity_pane
+
+ # create the TreeView using a filtered treestore
+ self.ftreestore = self.activity_updater.activity_list.filter_new()
+ if not _DEBUG_VIEW_ALL:
+ self.ftreestore.set_visible_column(model.UPDATE_EXISTS)
+ self.treeview = gtk.TreeView(self.ftreestore)
+
+ # create some cell renderers.
+ crbool = gtk.CellRendererToggle()
+ crbool.set_property('activatable', True)
+ crbool.set_property('xpad', style.DEFAULT_PADDING)
+ # indicator size should be themeable, but is not.
+ # if we're in sugar, use the larger indicator size.
+ # otherwise, use the hard-coded GTK default.
+ if self.activity_updater._in_sugar:
+ crbool.set_property('indicator_size', style.zoom(26))
+ def toggled_cb(crbool, path, self):
+ path = self.ftreestore.convert_path_to_child_path(path)
+ self.activity_updater.activity_list.toggle_select(path)
+ self.activity_pane._refresh_update_size()
+ crbool.connect('toggled', toggled_cb, self)
+
+ cricon = gtk.CellRendererPixbuf()
+ cricon.set_property('width', style.STANDARD_ICON_SIZE)
+ cricon.set_property('height', style.STANDARD_ICON_SIZE)
+
+ 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.ACTIVITY_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.DESCRIPTION_BIG))
+ if m.get_value(it, model.IS_HEADER):
+ s = '<big>%s</big>' % s
+ desc = m.get_value(it, model.DESCRIPTION_SMALL)
+ 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(activity_list.is_valid())
+ self.activity_updater.activity_list.connect('notify::is-valid',
+ is_valid_cb)
+ is_valid_cb(self.activity_updater.activity_list, None)
+
+ # now put this all inside ourself (a gtk.ScrolledWindow)
+ self.add_with_viewport(self.treeview)
+ self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+
+ def unlink_model(self):
+ self.treeview.set_model(None)
+ def relink_model(self):
+ self.ftreestore.refilter()
+ self.treeview.set_model(self.ftreestore)
+
+ 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:
+ activity_list = self.activity_updater.activity_list
+ def cb(__, f):
+ f()
+ self.activity_pane._refresh_update_size()
+ menu = gtk.Menu()
+ item_select_none = gtk.MenuItem(_("_Uncheck All"))
+ item_select_none.connect("activate", cb,
+ activity_list.unselect_all)
+ menu.add(item_select_none)
+ if 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,
+ activity_list.select_all)
+ menu.add(item_select_all)
+ menu.popup(None, None, None, 0, event.time)
+ menu.show_all()
+ return True
+ return False
+
+class GroupListView(gtk.VBox):
+ """List view in expander at bottom, showing groups and urls."""
+ def __init__(self, activity_updater):
+ gtk.VBox.__init__(self)
+ self.set_spacing(style.DEFAULT_PADDING)
+ self.activity_updater = activity_updater
+
+ self.fgroupstore = self.activity_updater.activity_list.filter_new()
+ def group_visibility(m, it, user_data=None):
+ # only group header rows, but not the 'local activities' group.
+ return m.get_value(it, model.IS_HEADER) and \
+ m.get_value(it, model.UPDATE_URL) is not None
+ self.fgroupstore.set_visible_func(group_visibility)
+ self.groupview = gtk.TreeView(self.fgroupstore)
+ self.groupview.set_headers_visible(False)
+ self.groupview.set_rules_hint(True)
+ self.groupview.set_enable_search(False)
+ self.groupview.set_reorderable(False) # we'll write own DnD funcs below
+ TARGETS = gtk.target_list_add_text_targets(info=0)
+ TARGETS = gtk.target_list_add_uri_targets(list=TARGETS, info=1)
+ self.groupview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
+ TARGETS,
+ gtk.gdk.ACTION_DEFAULT
+ |gtk.gdk.ACTION_COPY
+ |gtk.gdk.ACTION_MOVE)
+ self.groupview.enable_model_drag_dest(TARGETS,
+ gtk.gdk.ACTION_DEFAULT
+ |gtk.gdk.ACTION_COPY
+ |gtk.gdk.ACTION_MOVE)
+ def drag_data_get_data(groupview, context, drag_selection, target_id, etime):
+ selection = groupview.get_selection()
+ m, it = selection.get_selected()
+ gurl = m.get_value(it, model.UPDATE_URL)
+ drag_selection.set_text(gurl)
+ drag_selection.set_uris([gurl])
+ # for info on our DnD strategy here, see
+ # http://lists-archives.org/gtk/06954-treemodelfilter-drag-and-drop.html
+ def drag_drop(groupview, context, x, y, time, data=None):
+ targets = groupview.drag_dest_get_target_list()
+ t = groupview.drag_dest_find_target(context, targets)
+ drag_selection = groupview.drag_get_data(context, t)
+ return True # don't invoke parent.
+ def drag_data_received_data(groupview, context, x, y, drag_selection,
+ info, etime):
+ # weird gtk workaround; see mailing list post referenced above.
+ groupview.emit_stop_by_name('drag_data_received')
+ # ok, now see if this is a move from the same widget
+ same_widget = (context.get_source_widget() == groupview)
+ m = groupview.get_model()
+ gurl = drag_selection.data
+ drop_info = groupview.get_dest_row_at_pos(x, y)
+ if drop_info is None:
+ desired_group_num = -1 # at end.
+ else:
+ path, position = drop_info
+ path = self.fgroupstore.convert_path_to_child_path(path)
+ desired_group_num = self.activity_updater\
+ .activity_list[path][model.GROUP_NUM]
+ if position == gtk.TREE_VIEW_DROP_AFTER \
+ or position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER:
+ desired_group_num += 1
+ if not same_widget:
+ self.activity_updater.activity_list.add_group(gurl)
+ success = self.activity_updater.activity_list\
+ .move_group(gurl, desired_group_num)
+ want_delete = (context.action == gtk.gdk.ACTION_MOVE) and \
+ not same_widget # we'll handle the delete ourselves
+ context.finish(success, want_delete and success, etime)
+ return True # don't invoke parent.
+ self.groupview.connect("drag_data_get", drag_data_get_data)
+ self.groupview.connect("drag_data_received", drag_data_received_data)
+ self.groupview.connect("drag_drop", drag_drop)
+ crtext = gtk.CellRendererText()
+ column = gtk.TreeViewColumn('Name', crtext)
+ def group_name_markup(cell_layout, renderer, m, it):
+ renderer.set_property('markup', '<b>%s</b>\n<small>%s</small>' % \
+ (_e(m.get_value(it, model.DESCRIPTION_BIG)),
+ _e(m.get_value(it, model.UPDATE_URL))))
+ column.set_cell_data_func(crtext, group_name_markup)
+ self.groupview.append_column(column)
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.add_with_viewport(self.groupview)
+ scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+
+ hbox2 = gtk.HBox()
+ hbox2.set_spacing(style.DEFAULT_PADDING)
+ label = gtk.Label(_('Group URL:'))
+ hbox2.pack_start(label, expand=False)
+ self.group_entry = gtk.Entry()
+ hbox2.pack_start(self.group_entry, expand=True)
+ self.group_add_button = gtk.Button(stock=gtk.STOCK_ADD)
+ self.group_del_button = gtk.Button(stock=gtk.STOCK_REMOVE)
+ self.group_del_button.set_sensitive(False)
+ hbox2.pack_start(self.group_add_button, expand=False)
+ hbox2.pack_start(self.group_del_button, expand=False)
+
+ selection = self.groupview.get_selection()
+ selection.set_mode(gtk.SELECTION_SINGLE)
+ def group_select_cb(selection):
+ (m, it) = selection.get_selected()
+ if it is None: return
+ self.group_entry.set_text(m.get_value(it, model.UPDATE_URL))
+ def group_entry_changed_cb(group_entry):
+ selection = self.groupview.get_selection()
+ (m, it) = selection.get_selected()
+ if it is not None and \
+ group_entry.get_text() == m.get_value(it, model.UPDATE_URL):
+ is_add = False
+ else:
+ is_add = True
+ selection.unselect_all()
+ self.group_add_button.set_sensitive(is_add)
+ self.group_del_button.set_sensitive(not is_add)
+ selection.connect('changed', group_select_cb)
+ self.group_entry.connect('changed', group_entry_changed_cb)
+ self.group_entry.connect('activate', self._add_group_cb, self)
+ self.group_add_button.connect('clicked', self._add_group_cb, self)
+ self.group_del_button.connect('clicked', self._del_group_cb, self)
+
+ self.pack_start(scrolled_window, expand=True)
+ self.pack_start(hbox2, expand=False)
+
+ def _add_group_cb(self, widget, event, data=None):
+ success = self.activity_updater.activity_list\
+ .add_group(self.group_entry.get_text())
+ if success:
+ self.group_entry.set_text('')
+ self.groupview.scroll_to_cell(self.fgroupstore[-1].path)
+
+ def _del_group_cb(self, widget, event, data=None):
+ success = self.activity_updater.activity_list\
+ .del_group(self.group_entry.get_text())
+ if success:
+ self.group_entry.set_text('')
+
+ def unlink_model(self):
+ self.groupview.set_model(None)
+
+ def relink_model(self):
+ self.fgroupstore.refilter()
+ self.groupview.set_model(self.fgroupstore)
+
+class ActivityPane(gtk.VBox):
+ """Container for the activity and group lists."""
+
+ def __init__(self, activity_updater):
+ gtk.VBox.__init__(self)
+ self.activity_updater = activity_updater
+ self.set_spacing(style.DEFAULT_PADDING)
+
+ ## activity list at top
+ vpaned = gtk.VPaned()
+ self.activities = ActivityListView(activity_updater, self)
+ vpaned.pack1(self.activities, resize=True, shrink=False)
+
+ ## expander/group list view at bottom
+ self.expander = gtk.Expander(label=_('Modify activity groups'))
+ self.expander.set_use_markup(True)
+ def expander_callback(expander, __):
+ if not expander.get_expanded(): # when groups are collapsed...
+ vpaned.set_position(-1) # ...unset VPaned thumb
+ self.expander.connect('notify::expanded', expander_callback)
+ self.groups = GroupListView(activity_updater)
+ self.expander.add(self.groups)
+ vpaned.pack2(self.expander, resize=False, shrink=False)
+ self.pack_start(vpaned, expand=True)
+
+ ### Install/refresh buttons below these.
+ 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', activity_updater.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', activity_updater.download_cb, self)
+ button_box.pack_start(self.install_button, expand=False)
+ def is_valid_cb(activity_list, __):
+ self.install_button.set_sensitive(activity_list.is_valid())
+ activity_updater.activity_list.connect('notify::is-valid', is_valid_cb)
+
+ def unlink_models(self):
+ self.activities.unlink_model()
+ self.groups.unlink_model()
+
+ def relink_models(self):
+ self.activities.relink_model()
+ self.groups.relink_model()
+
+ def _refresh_update_size(self):
+ """Update the 'download size' label."""
+ activity_list = self.activity_updater.activity_list
+ size = activity_list.updates_size()
+ self.size_label.set_markup(_('Download size: %s') %
+ _humanize_size(size))
+ self.install_button.set_sensitive(activity_list.updates_selected()!=0)
+
+ def switch(self):
+ """Make the activity list visible and the progress pane invisible."""
+ for widget, v in [ (self, True),
+ (self.activity_updater.progress_pane, False),
+ (self.activity_updater.expander, False)]:
+ widget.set_property('visible', v)
+
+class ProgressPane(gtk.VBox):
+ """Container which replaces the `ActivityPane` during refresh or
+ install."""
+
+ def __init__(self, activity_updater):
+ self.activity_updater = activity_updater
+ 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) # center the label.
+ 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, activity_updater.cancel_cb),
+ (self.refresh_button, activity_updater.refresh_cb),
+ (self.try_again_button, activity_updater.refresh_cb)]:
+ widget.connect('clicked', cb, activity_updater)
+ 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 ''
+ if False and n is not None: # XXX 'percentage' disabled; it looks bad.
+ if len(extra) > 0: extra += ' '
+ extra += '%.0f%%' % (n*100)
+ 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 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.activity_updater.activity_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.activity_updater.expander, False)]:
+ widget.set_property('visible', v)
+ self.cancel_button.set_sensitive(True)
+ self.activity_updater.expander.set_expanded(False)
+
+ 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, msg, try_again=False):
+ self._switch(show_cancel=False, show_bar=False,
+ show_try_again=try_again)
+ self.label.set_markup(msg)
+ self.activity_updater.expander.set_property('visible', True)
+
+class ActivityUpdater(SectionView):
+ """Software update control panel UI class."""
+
+ def __init__(self, modelwrapper, alerts):
+ SectionView.__init__(self)
+ self._in_sugar = (modelwrapper is not None)
+ self.set_spacing(style.DEFAULT_SPACING)
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+
+ # top labels.
+ 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.set_markup('<big>%s</big>'%_('Checking for updates...'))
+ bottom_label = gtk.Label()
+ bottom_label.set_line_wrap(True) # doesn't really work right =(
+ 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.'))
+ vbox2 = gtk.VBox()
+ vbox2.pack_start(self.top_label, expand=False)
+ vbox2.pack_start(gtk.HSeparator(), expand=False)
+ vbox2.pack_start(bottom_label, expand=True)
+ self.pack_start(vbox2, expand=False)
+
+ # activity/group pane ####
+ self.activity_list = model.UpdateList()
+ self.activity_pane = ActivityPane(self)
+ self.pack_start(self.activity_pane, expand=True)
+
+ # progress pane ###########
+ self.progress_pane = ProgressPane(self)
+ self.pack_start(self.progress_pane, expand=True, fill=False)
+
+ # special little extension to progress pane.
+ self.expander = gtk.Expander(label=_('Modify activity groups'))
+ def expander_cb(expander, param_):
+ if expander.get_expanded():
+ self.activity_pane.switch()
+ self.activity_pane.expander.set_expanded(True)
+ expander.set_expanded(False)
+ self.expander.connect("notify::expanded", expander_cb)
+ self.pack_end(self.expander, expand=False)
+
+ # show our work!
+ self.show_all()
+ # and start refreshing.
+ self.refresh_cb(None, None)
+
+ def download_cb(self, widget, event, data=None):
+ """Invoked when the 'ok' button is clicked."""
+ from sugar.bundle.activitybundle import ActivityBundle
+ self.top_label.set_markup('<big>%s</big>' %
+ _('Downloading updates...'))
+ self.progress_pane.switch_to_download_progress()
+ self._progress_cb(0, _('Starting download...'))
+ self._cancel_func = lambda: self.activity_list.cancel_download()
+ def progress_cb(n, extra=None, icon=None):
+ gobject.idle_add(self._progress_cb, n, extra, icon)
+ @inhibit_suspend
+ def do_download():
+ # ensure main loop is dbus-registered
+ from dbus.mainloop.glib import DBusGMainLoop
+ DBusGMainLoop(set_as_default=True)
+ # get activity registry
+ from jarabe.model.bundleregistry import get_registry
+ registry = get_registry() # requires a dbus-registered main loop
+ # progress bar bookkeeping.
+ counts = [0, self.activity_list.updates_selected(), 0]
+ def p(n, extra, icon):
+ if n is None:
+ progress_cb(n, extra, icon)
+ else:
+ progress_cb((n+(counts[0]/counts[1]))/2, extra, icon)
+ counts[2] = n # last fraction.
+ def q(n, row):
+ p(n, _('Downloading %s...') % row[model.DESCRIPTION_BIG],
+ row[model.ACTIVITY_ICON])
+ for row, f in self.activity_list.download_selected_updates(q):
+ if f is None: continue # cancelled or network error.
+ try:
+ p(counts[2], _('Examining %s...')%row[model.DESCRIPTION_BIG],
+ row[model.ACTIVITY_ICON])
+ b = actutils.BundleHelper(f)
+ p(counts[2], _('Installing %s...') % b.get_name(),
+ _svg2pixbuf(b.get_icon_data()))
+ b.install_or_upgrade(registry)
+ except:
+ pass # XXX: use alert to indicate install failure.
+ if os.path.exists(f):
+ os.unlink(f)
+ counts[0]+=1
+ # refresh when we're done.
+ gobject.idle_add(self.refresh_cb, None, None, False)
+ Thread(target=do_download).start()
+
+ def cancel_cb(self, widget, event, data=None):
+ """Callback when the cancel button is clicked."""
+ self.progress_pane.cancelling()
+ self._cancel_func()
+
+ def refresh_cb(self, widget, event, clear_cache=True):
+ """Invoked when the 'refresh' button is clicked."""
+ self.top_label.set_markup('<big>%s</big>' %
+ _('Checking for updates...'))
+ self.progress_pane.switch_to_check_progress()
+ self._cancel_func = lambda: self.activity_list.cancel_refresh()
+ # unlink model from treeview, and perform actual refresh in another
+ # thread.
+ self.activity_pane.unlink_models()
+ # freeze notify queue for activity_list to prevent thread problems.
+ self.activity_list.freeze_notify()
+ def progress_cb(n, extra=None):
+ gobject.idle_add(self._progress_cb, n, extra)
+ @inhibit_suspend
+ def do_refresh():
+ self.activity_list.refresh(progress_cb, clear_cache=clear_cache)
+ gobject.idle_add(self._refresh_done_cb)
+ Thread(target=do_refresh).start()
+ return False
+
+ def _progress_cb(self, n, extra=None, icon=None):
+ """Invoked in main thread during a refresh operation."""
+ self.progress_pane.update(n, extra, icon)
+ return False
+
+ def _refresh_done_cb(self):
+ """Invoked in main thread when the refresh is complete."""
+ self.activity_pane.relink_models()
+ self.activity_list.thaw_notify()
+ # clear the group url
+ self.activity_pane.groups.group_entry.set_text('')
+ # so, how did we do?
+ if not self.activity_list.saw_network_success():
+ header = _("Could not access the network")
+ self.progress_pane.switch_to_complete_message(
+ _('Could not access the network to check for updates.'),
+ try_again=True)
+ else:
+ avail = self.activity_list.updates_available()
+ if avail == 0:
+ header = _("Your software is up-to-date")
+ self.progress_pane.switch_to_complete_message(header)
+ else:
+ header = gettext.ngettext("You can install %s update",
+ "You can install %s updates", avail) \
+ % avail
+ self.activity_pane.switch()
+ self.top_label.set_markup('<big>%s</big>' % _e(header))
+ self.activity_pane._refresh_update_size()
+ # XXX: auto-close is disabled; I'm not convinced it helps the UI
+ if False and self.auto_close and \
+ self.activity_list.saw_network_success() and avail == 0:
+ # okay, automatically close since no updates were found.
+ self.emit('request-close')
+ return False
+
+ def delete_event(self, widget, event, data=None):
+ return False # destroy me!
+
+ def destroy(self, widget, data=None):
+ gtk.main_quit()
+
+ def main(self):
+ """Start gtk main loop."""
+ gtk.main()
+
+def _main():
+ """Testing code; runs updater standalone."""
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.set_title(TITLE)
+ window.set_size_request(425, 400)
+ au = ActivityUpdater(None, None)
+ au.connect('request-close', au.destroy) # auto-close == destroy.
+ window.connect('delete_event', au.delete_event)
+ window.connect('destroy', au.destroy)
+ window.add(au)
+ au.set_border_width(style.DEFAULT_SPACING) # our window is smaller here.
+ window.show()
+ gtk.main()
+
+# set PYTHONPATH to /usr/share/sugar/shell before invoking.
+if __name__ == '__main__': _main ()
diff --git a/sugar-update-control.spec b/sugar-update-control.spec
index 0979782..262a355 100644
--- a/sugar-update-control.spec
+++ b/sugar-update-control.spec
@@ -12,7 +12,7 @@ Group: System Environment/Base
URL: http://dev.laptop.org/git?p=users/cscott/sugar-update-control
Source0: %{name}-%{version}.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
-Requires: %{PYTHON_NAME} >= %{PYTHON_VERSION}, %{PYTHON_NAME} < %{NEXT_PYTHON_VERSION}, olpc-update >= 2.13, sugar >= 0.81.8
+Requires: %{PYTHON_NAME} >= %{PYTHON_VERSION}, %{PYTHON_NAME} < %{NEXT_PYTHON_VERSION}, olpc-update >= 2.13, sugar >= 0.83.0
BuildPrereq: %{PYTHON_NAME}-devel >= 2.5
BuildArch: noarch
diff --git a/view/updater.py b/view/updater.py
deleted file mode 100755
index 9e2d6d5..0000000
--- a/view/updater.py
+++ /dev/null
@@ -1,704 +0,0 @@
-#!/usr/bin/python2.5
-# Copyright (C) 2008 One Laptop Per Child Association, Inc.
-# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
-# Written by C. Scott Ananian <cscott at laptop.org>
-"""Activity updater.
-
-Checks for updates to activities and installs them."""
-from __future__ import with_statement
-from __future__ import division
-import pygtk
-pygtk.require('2.0')
-import gtk, gobject
-gtk.gdk.threads_init()
-
-import gettext
-import os
-import re
-from threading import Thread
-from gettext import gettext as _
-
-import bitfrost.update.actutils as actutils
-from sugar.graphics import style
-
-from controlpanel.sectionview import SectionView
-from controlpanel.inlinealert import InlineAlert
-
-import controlpanel.model.updater as model
-from controlpanel.model.updater import _humanize_size, _svg2pixbuf, inhibit_suspend
-
-# COMPATIBILITY HACK: can be removed after sugar 0.81.9 is released.
-# (be sure to update sugar-update-control.spec w/ this dependency when so)
-from controlpanel.gui import ControlPanel
-if not hasattr(ControlPanel, 'set_section_view_auto_close'):
- OriginalSectionView = SectionView
- class TweakedSectionView(OriginalSectionView):
- __gsignals__ = {
- 'request-close': (gobject.SIGNAL_RUN_FIRST,
- gobject.TYPE_NONE, ([]))
- }
- def __init__(self):
- OriginalSectionView.__init__(self)
- self.auto_close = False
- SectionView = TweakedSectionView
-# END COMPATIBILITY HACK
-
-# COMPATIBILITY HACK: work around trac #8532 by forcibly removing the
-# SIGCHLD handler (and the necessity for one)
-import sugar.activity.activityfactory
-if hasattr(sugar.activity.activityfactory, '_sigchild_handler'):
- from ctypes import CDLL, Structure, c_voidp, c_int, c_ulong, pointer
- import signal
- libc = CDLL("libc.so.6")
- SA_NOCLDWAIT = 2 # according to /usr/include/bits/sigaction.h
- class SIGACTION(Structure):
- _fields_ = [('sa_handler', c_voidp),
- ('sa_sigaction', c_voidp),
- ('sa_mask', c_ulong), # sigset_t
- ('sa_flags', c_int),
- ('sa_restorer', c_voidp)]
- desired = SIGACTION(None, # SIG_DFL, according to /usr/include/asm-generic/signal.h
- None,
- 0,
- SA_NOCLDWAIT,
- None)
- libc.sigaction(signal.SIGCHLD, pointer(desired), None)
-# END COMPATIBILITY HACK
-
-
-# configuration constants needed for control panel framework
-CLASS = 'ActivityUpdater'
-ICON = 'module-updater'
-TITLE = _('Software update')
-
-# for debugging
-_DEBUG_VIEW_ALL=False
-"""View even activities with no pending updates."""
-
-def _escape_markup(s):
- """Escape special characters in `s` so that it is safe to use in
- Pango markup. Equivalent to `g_markup_escape_text` in glib."""
- entities = { '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''', }
- if s is None: return None
- return re.sub("[&<>\"']", lambda m: entities[m.group(0)],
- s.decode('utf-8','ignore')).encode('utf-8')
-_e = _escape_markup
-"""Useful abbreviation."""
-
-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
-
-### Pieces of the activity updater view; factored to make the UI structure
-### more apparent in `ActivityUpdater.__init__`.
-
-class ActivityListView(gtk.ScrolledWindow):
- """List view at the top, showing activities, versions, and sizes."""
- def __init__(self, activity_updater, activity_pane):
- gtk.ScrolledWindow.__init__(self)
- self.activity_updater = activity_updater
- self.activity_pane = activity_pane
-
- # create the TreeView using a filtered treestore
- self.ftreestore = self.activity_updater.activity_list.filter_new()
- if not _DEBUG_VIEW_ALL:
- self.ftreestore.set_visible_column(model.UPDATE_EXISTS)
- self.treeview = gtk.TreeView(self.ftreestore)
-
- # create some cell renderers.
- crbool = gtk.CellRendererToggle()
- crbool.set_property('activatable', True)
- crbool.set_property('xpad', style.DEFAULT_PADDING)
- # indicator size should be themeable, but is not.
- # if we're in sugar, use the larger indicator size.
- # otherwise, use the hard-coded GTK default.
- if self.activity_updater._in_sugar:
- crbool.set_property('indicator_size', style.zoom(26))
- def toggled_cb(crbool, path, self):
- path = self.ftreestore.convert_path_to_child_path(path)
- self.activity_updater.activity_list.toggle_select(path)
- self.activity_pane._refresh_update_size()
- crbool.connect('toggled', toggled_cb, self)
-
- cricon = gtk.CellRendererPixbuf()
- cricon.set_property('width', style.STANDARD_ICON_SIZE)
- cricon.set_property('height', style.STANDARD_ICON_SIZE)
-
- 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.ACTIVITY_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.DESCRIPTION_BIG))
- if m.get_value(it, model.IS_HEADER):
- s = '<big>%s</big>' % s
- desc = m.get_value(it, model.DESCRIPTION_SMALL)
- 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(activity_list.is_valid())
- self.activity_updater.activity_list.connect('notify::is-valid',
- is_valid_cb)
- is_valid_cb(self.activity_updater.activity_list, None)
-
- # now put this all inside ourself (a gtk.ScrolledWindow)
- self.add_with_viewport(self.treeview)
- self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-
- def unlink_model(self):
- self.treeview.set_model(None)
- def relink_model(self):
- self.ftreestore.refilter()
- self.treeview.set_model(self.ftreestore)
-
- 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:
- activity_list = self.activity_updater.activity_list
- def cb(__, f):
- f()
- self.activity_pane._refresh_update_size()
- menu = gtk.Menu()
- item_select_none = gtk.MenuItem(_("_Uncheck All"))
- item_select_none.connect("activate", cb,
- activity_list.unselect_all)
- menu.add(item_select_none)
- if 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,
- activity_list.select_all)
- menu.add(item_select_all)
- menu.popup(None, None, None, 0, event.time)
- menu.show_all()
- return True
- return False
-
-class GroupListView(gtk.VBox):
- """List view in expander at bottom, showing groups and urls."""
- def __init__(self, activity_updater):
- gtk.VBox.__init__(self)
- self.set_spacing(style.DEFAULT_PADDING)
- self.activity_updater = activity_updater
-
- self.fgroupstore = self.activity_updater.activity_list.filter_new()
- def group_visibility(m, it, user_data=None):
- # only group header rows, but not the 'local activities' group.
- return m.get_value(it, model.IS_HEADER) and \
- m.get_value(it, model.UPDATE_URL) is not None
- self.fgroupstore.set_visible_func(group_visibility)
- self.groupview = gtk.TreeView(self.fgroupstore)
- self.groupview.set_headers_visible(False)
- self.groupview.set_rules_hint(True)
- self.groupview.set_enable_search(False)
- self.groupview.set_reorderable(False) # we'll write own DnD funcs below
- TARGETS = gtk.target_list_add_text_targets(info=0)
- TARGETS = gtk.target_list_add_uri_targets(list=TARGETS, info=1)
- self.groupview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
- TARGETS,
- gtk.gdk.ACTION_DEFAULT
- |gtk.gdk.ACTION_COPY
- |gtk.gdk.ACTION_MOVE)
- self.groupview.enable_model_drag_dest(TARGETS,
- gtk.gdk.ACTION_DEFAULT
- |gtk.gdk.ACTION_COPY
- |gtk.gdk.ACTION_MOVE)
- def drag_data_get_data(groupview, context, drag_selection, target_id, etime):
- selection = groupview.get_selection()
- m, it = selection.get_selected()
- gurl = m.get_value(it, model.UPDATE_URL)
- drag_selection.set_text(gurl)
- drag_selection.set_uris([gurl])
- # for info on our DnD strategy here, see
- # http://lists-archives.org/gtk/06954-treemodelfilter-drag-and-drop.html
- def drag_drop(groupview, context, x, y, time, data=None):
- targets = groupview.drag_dest_get_target_list()
- t = groupview.drag_dest_find_target(context, targets)
- drag_selection = groupview.drag_get_data(context, t)
- return True # don't invoke parent.
- def drag_data_received_data(groupview, context, x, y, drag_selection,
- info, etime):
- # weird gtk workaround; see mailing list post referenced above.
- groupview.emit_stop_by_name('drag_data_received')
- # ok, now see if this is a move from the same widget
- same_widget = (context.get_source_widget() == groupview)
- m = groupview.get_model()
- gurl = drag_selection.data
- drop_info = groupview.get_dest_row_at_pos(x, y)
- if drop_info is None:
- desired_group_num = -1 # at end.
- else:
- path, position = drop_info
- path = self.fgroupstore.convert_path_to_child_path(path)
- desired_group_num = self.activity_updater\
- .activity_list[path][model.GROUP_NUM]
- if position == gtk.TREE_VIEW_DROP_AFTER \
- or position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER:
- desired_group_num += 1
- if not same_widget:
- self.activity_updater.activity_list.add_group(gurl)
- success = self.activity_updater.activity_list\
- .move_group(gurl, desired_group_num)
- want_delete = (context.action == gtk.gdk.ACTION_MOVE) and \
- not same_widget # we'll handle the delete ourselves
- context.finish(success, want_delete and success, etime)
- return True # don't invoke parent.
- self.groupview.connect("drag_data_get", drag_data_get_data)
- self.groupview.connect("drag_data_received", drag_data_received_data)
- self.groupview.connect("drag_drop", drag_drop)
- crtext = gtk.CellRendererText()
- column = gtk.TreeViewColumn('Name', crtext)
- def group_name_markup(cell_layout, renderer, m, it):
- renderer.set_property('markup', '<b>%s</b>\n<small>%s</small>' % \
- (_e(m.get_value(it, model.DESCRIPTION_BIG)),
- _e(m.get_value(it, model.UPDATE_URL))))
- column.set_cell_data_func(crtext, group_name_markup)
- self.groupview.append_column(column)
- scrolled_window = gtk.ScrolledWindow()
- scrolled_window.add_with_viewport(self.groupview)
- scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-
- hbox2 = gtk.HBox()
- hbox2.set_spacing(style.DEFAULT_PADDING)
- label = gtk.Label(_('Group URL:'))
- hbox2.pack_start(label, expand=False)
- self.group_entry = gtk.Entry()
- hbox2.pack_start(self.group_entry, expand=True)
- self.group_add_button = gtk.Button(stock=gtk.STOCK_ADD)
- self.group_del_button = gtk.Button(stock=gtk.STOCK_REMOVE)
- self.group_del_button.set_sensitive(False)
- hbox2.pack_start(self.group_add_button, expand=False)
- hbox2.pack_start(self.group_del_button, expand=False)
-
- selection = self.groupview.get_selection()
- selection.set_mode(gtk.SELECTION_SINGLE)
- def group_select_cb(selection):
- (m, it) = selection.get_selected()
- if it is None: return
- self.group_entry.set_text(m.get_value(it, model.UPDATE_URL))
- def group_entry_changed_cb(group_entry):
- selection = self.groupview.get_selection()
- (m, it) = selection.get_selected()
- if it is not None and \
- group_entry.get_text() == m.get_value(it, model.UPDATE_URL):
- is_add = False
- else:
- is_add = True
- selection.unselect_all()
- self.group_add_button.set_sensitive(is_add)
- self.group_del_button.set_sensitive(not is_add)
- selection.connect('changed', group_select_cb)
- self.group_entry.connect('changed', group_entry_changed_cb)
- self.group_entry.connect('activate', self._add_group_cb, self)
- self.group_add_button.connect('clicked', self._add_group_cb, self)
- self.group_del_button.connect('clicked', self._del_group_cb, self)
-
- self.pack_start(scrolled_window, expand=True)
- self.pack_start(hbox2, expand=False)
-
- def _add_group_cb(self, widget, event, data=None):
- success = self.activity_updater.activity_list\
- .add_group(self.group_entry.get_text())
- if success:
- self.group_entry.set_text('')
- self.groupview.scroll_to_cell(self.fgroupstore[-1].path)
-
- def _del_group_cb(self, widget, event, data=None):
- success = self.activity_updater.activity_list\
- .del_group(self.group_entry.get_text())
- if success:
- self.group_entry.set_text('')
-
- def unlink_model(self):
- self.groupview.set_model(None)
-
- def relink_model(self):
- self.fgroupstore.refilter()
- self.groupview.set_model(self.fgroupstore)
-
-class ActivityPane(gtk.VBox):
- """Container for the activity and group lists."""
-
- def __init__(self, activity_updater):
- gtk.VBox.__init__(self)
- self.activity_updater = activity_updater
- self.set_spacing(style.DEFAULT_PADDING)
-
- ## activity list at top
- vpaned = gtk.VPaned()
- self.activities = ActivityListView(activity_updater, self)
- vpaned.pack1(self.activities, resize=True, shrink=False)
-
- ## expander/group list view at bottom
- self.expander = gtk.Expander(label=_('Modify activity groups'))
- self.expander.set_use_markup(True)
- def expander_callback(expander, __):
- if not expander.get_expanded(): # when groups are collapsed...
- vpaned.set_position(-1) # ...unset VPaned thumb
- self.expander.connect('notify::expanded', expander_callback)
- self.groups = GroupListView(activity_updater)
- self.expander.add(self.groups)
- vpaned.pack2(self.expander, resize=False, shrink=False)
- self.pack_start(vpaned, expand=True)
-
- ### Install/refresh buttons below these.
- 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', activity_updater.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', activity_updater.download_cb, self)
- button_box.pack_start(self.install_button, expand=False)
- def is_valid_cb(activity_list, __):
- self.install_button.set_sensitive(activity_list.is_valid())
- activity_updater.activity_list.connect('notify::is-valid', is_valid_cb)
-
- def unlink_models(self):
- self.activities.unlink_model()
- self.groups.unlink_model()
-
- def relink_models(self):
- self.activities.relink_model()
- self.groups.relink_model()
-
- def _refresh_update_size(self):
- """Update the 'download size' label."""
- activity_list = self.activity_updater.activity_list
- size = activity_list.updates_size()
- self.size_label.set_markup(_('Download size: %s') %
- _humanize_size(size))
- self.install_button.set_sensitive(activity_list.updates_selected()!=0)
-
- def switch(self):
- """Make the activity list visible and the progress pane invisible."""
- for widget, v in [ (self, True),
- (self.activity_updater.progress_pane, False),
- (self.activity_updater.expander, False)]:
- widget.set_property('visible', v)
-
-class ProgressPane(gtk.VBox):
- """Container which replaces the `ActivityPane` during refresh or
- install."""
-
- def __init__(self, activity_updater):
- self.activity_updater = activity_updater
- 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) # center the label.
- 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, activity_updater.cancel_cb),
- (self.refresh_button, activity_updater.refresh_cb),
- (self.try_again_button, activity_updater.refresh_cb)]:
- widget.connect('clicked', cb, activity_updater)
- 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 ''
- if False and n is not None: # XXX 'percentage' disabled; it looks bad.
- if len(extra) > 0: extra += ' '
- extra += '%.0f%%' % (n*100)
- 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 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.activity_updater.activity_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.activity_updater.expander, False)]:
- widget.set_property('visible', v)
- self.cancel_button.set_sensitive(True)
- self.activity_updater.expander.set_expanded(False)
-
- 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, msg, try_again=False):
- self._switch(show_cancel=False, show_bar=False,
- show_try_again=try_again)
- self.label.set_markup(msg)
- self.activity_updater.expander.set_property('visible', True)
-
-class ActivityUpdater(SectionView):
- """Software update control panel UI class."""
-
- def __init__(self, modelwrapper, alerts):
- SectionView.__init__(self)
- self._in_sugar = (modelwrapper is not None)
- self.set_spacing(style.DEFAULT_SPACING)
- self.set_border_width(style.DEFAULT_SPACING * 2)
-
- # top labels.
- 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.set_markup('<big>%s</big>'%_('Checking for updates...'))
- bottom_label = gtk.Label()
- bottom_label.set_line_wrap(True) # doesn't really work right =(
- 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.'))
- vbox2 = gtk.VBox()
- vbox2.pack_start(self.top_label, expand=False)
- vbox2.pack_start(gtk.HSeparator(), expand=False)
- vbox2.pack_start(bottom_label, expand=True)
- self.pack_start(vbox2, expand=False)
-
- # activity/group pane ####
- self.activity_list = model.UpdateList()
- self.activity_pane = ActivityPane(self)
- self.pack_start(self.activity_pane, expand=True)
-
- # progress pane ###########
- self.progress_pane = ProgressPane(self)
- self.pack_start(self.progress_pane, expand=True, fill=False)
-
- # special little extension to progress pane.
- self.expander = gtk.Expander(label=_('Modify activity groups'))
- def expander_cb(expander, param_):
- if expander.get_expanded():
- self.activity_pane.switch()
- self.activity_pane.expander.set_expanded(True)
- expander.set_expanded(False)
- self.expander.connect("notify::expanded", expander_cb)
- self.pack_end(self.expander, expand=False)
-
- # show our work!
- self.show_all()
- # and start refreshing.
- self.refresh_cb(None, None)
-
- def download_cb(self, widget, event, data=None):
- """Invoked when the 'ok' button is clicked."""
- from sugar.bundle.activitybundle import ActivityBundle
- self.top_label.set_markup('<big>%s</big>' %
- _('Downloading updates...'))
- self.progress_pane.switch_to_download_progress()
- self._progress_cb(0, _('Starting download...'))
- self._cancel_func = lambda: self.activity_list.cancel_download()
- def progress_cb(n, extra=None, icon=None):
- gobject.idle_add(self._progress_cb, n, extra, icon)
- @inhibit_suspend
- def do_download():
- # ensure main loop is dbus-registered
- from dbus.mainloop.glib import DBusGMainLoop
- DBusGMainLoop(set_as_default=True)
- # get activity registry
- from sugar.activity.registry import get_registry
- registry = get_registry() # requires a dbus-registered main loop
- # progress bar bookkeeping.
- counts = [0, self.activity_list.updates_selected(), 0]
- def p(n, extra, icon):
- if n is None:
- progress_cb(n, extra, icon)
- else:
- progress_cb((n+(counts[0]/counts[1]))/2, extra, icon)
- counts[2] = n # last fraction.
- def q(n, row):
- p(n, _('Downloading %s...') % row[model.DESCRIPTION_BIG],
- row[model.ACTIVITY_ICON])
- for row, f in self.activity_list.download_selected_updates(q):
- if f is None: continue # cancelled or network error.
- try:
- p(counts[2], _('Examining %s...')%row[model.DESCRIPTION_BIG],
- row[model.ACTIVITY_ICON])
- b = actutils.BundleHelper(f)
- p(counts[2], _('Installing %s...') % b.get_name(),
- _svg2pixbuf(b.get_icon_data()))
- b.install_or_upgrade(registry)
- except:
- pass # XXX: use alert to indicate install failure.
- if os.path.exists(f):
- os.unlink(f)
- counts[0]+=1
- # refresh when we're done.
- gobject.idle_add(self.refresh_cb, None, None, False)
- Thread(target=do_download).start()
-
- def cancel_cb(self, widget, event, data=None):
- """Callback when the cancel button is clicked."""
- self.progress_pane.cancelling()
- self._cancel_func()
-
- def refresh_cb(self, widget, event, clear_cache=True):
- """Invoked when the 'refresh' button is clicked."""
- self.top_label.set_markup('<big>%s</big>' %
- _('Checking for updates...'))
- self.progress_pane.switch_to_check_progress()
- self._cancel_func = lambda: self.activity_list.cancel_refresh()
- # unlink model from treeview, and perform actual refresh in another
- # thread.
- self.activity_pane.unlink_models()
- # freeze notify queue for activity_list to prevent thread problems.
- self.activity_list.freeze_notify()
- def progress_cb(n, extra=None):
- gobject.idle_add(self._progress_cb, n, extra)
- @inhibit_suspend
- def do_refresh():
- self.activity_list.refresh(progress_cb, clear_cache=clear_cache)
- gobject.idle_add(self._refresh_done_cb)
- Thread(target=do_refresh).start()
- return False
-
- def _progress_cb(self, n, extra=None, icon=None):
- """Invoked in main thread during a refresh operation."""
- self.progress_pane.update(n, extra, icon)
- return False
-
- def _refresh_done_cb(self):
- """Invoked in main thread when the refresh is complete."""
- self.activity_pane.relink_models()
- self.activity_list.thaw_notify()
- # clear the group url
- self.activity_pane.groups.group_entry.set_text('')
- # so, how did we do?
- if not self.activity_list.saw_network_success():
- header = _("Could not access the network")
- self.progress_pane.switch_to_complete_message(
- _('Could not access the network to check for updates.'),
- try_again=True)
- else:
- avail = self.activity_list.updates_available()
- if avail == 0:
- header = _("Your software is up-to-date")
- self.progress_pane.switch_to_complete_message(header)
- else:
- header = gettext.ngettext("You can install %s update",
- "You can install %s updates", avail) \
- % avail
- self.activity_pane.switch()
- self.top_label.set_markup('<big>%s</big>' % _e(header))
- self.activity_pane._refresh_update_size()
- # XXX: auto-close is disabled; I'm not convinced it helps the UI
- if False and self.auto_close and \
- self.activity_list.saw_network_success() and avail == 0:
- # okay, automatically close since no updates were found.
- self.emit('request-close')
- return False
-
- def delete_event(self, widget, event, data=None):
- return False # destroy me!
-
- def destroy(self, widget, data=None):
- gtk.main_quit()
-
- def main(self):
- """Start gtk main loop."""
- gtk.main()
-
-def _main():
- """Testing code; runs updater standalone."""
- window = gtk.Window(gtk.WINDOW_TOPLEVEL)
- window.set_title(TITLE)
- window.set_size_request(425, 400)
- au = ActivityUpdater(None, None)
- au.connect('request-close', au.destroy) # auto-close == destroy.
- window.connect('delete_event', au.delete_event)
- window.connect('destroy', au.destroy)
- window.add(au)
- au.set_border_width(style.DEFAULT_SPACING) # our window is smaller here.
- window.show()
- gtk.main()
-
-# set PYTHONPATH to /usr/share/sugar/shell before invoking.
-if __name__ == '__main__': _main ()
-----------------------------------------------------------------------
--
/home/cscott/public_git/sugar-update-control
More information about the Commits
mailing list