[Sugar-devel] [PATCH Maze 4/4] Save and restore state of the game
Manuel Kaufmann
humitos at gmail.com
Tue Mar 20 21:52:54 EDT 2012
Ability to 'save' (when the user closes the Activity) and 'restore' (when the
user launch it from the Journal or the Home without holding Alt) the state of
the game.
For this ability I had to upgrade 'olpcgames' to 1.6 because
'olpcgames.FILE_READ_REQUEST' and 'olpcgames.FILE_WRITE_REQUEST' events are
added in that version and those events are needed for this.
The data is saved (as JSON, with simplejson module) in the 'event.filename'
path using the 'sugar.datastore' API.
A filename example is:
~/.sugar/default/datastore/09/0940b7eb-1cb7-4687-aa7a-ac2c174dcfd7/data
This commit solves ticket #2393:
* http://bugs.sugarlabs.org/ticket/2393
---
game.py | 77 +++++-
olpcgames/__init__.py | 96 ++++++--
olpcgames/_cairoimage.py | 97 +++++++-
olpcgames/_gtkmain.py | 70 +++++
olpcgames/_version.py | 2 +
olpcgames/activity.py | 230 ++++++++++--------
olpcgames/buildmanifest.py | 33 +++
olpcgames/camera.py | 186 +++++++--------
olpcgames/canvas.py | 99 ++++++--
olpcgames/data/sleeping_svg.py | 550 ++++------------------------------------
olpcgames/dbusproxy.py | 93 +++++++
olpcgames/eventwrap.py | 292 ++++++++++++++++++----
olpcgames/gtkEvent.py | 23 ++-
olpcgames/mesh.py | 451 ++++++++++++++++++++++----------
olpcgames/mybutton.py | 53 ----
olpcgames/pangofont.py | 159 ++++++++----
olpcgames/pausescreen.py | 53 +++-
olpcgames/svgsprite.py | 25 ++-
olpcgames/textsprite.py | 40 +++
olpcgames/util.py | 27 ++-
olpcgames/video.py | 12 +-
21 files changed, 1589 insertions(+), 1079 deletions(-)
create mode 100644 olpcgames/_gtkmain.py
create mode 100644 olpcgames/_version.py
create mode 100755 olpcgames/buildmanifest.py
create mode 100644 olpcgames/dbusproxy.py
delete mode 100644 olpcgames/mybutton.py
create mode 100644 olpcgames/textsprite.py
diff --git a/game.py b/game.py
index 579c5c4..1e23d13 100644
--- a/game.py
+++ b/game.py
@@ -23,13 +23,23 @@
import sys
import time
-
+import simplejson
+import tempfile
+import shutil
import pygame
+import olpcgames
+
+import logging
+log = logging.getLogger('Maze')
+log.setLevel(logging.DEBUG)
+logging.basicConfig()
+
import olpcgames.pausescreen as pausescreen
import olpcgames.mesh as mesh
from olpcgames.util import get_bundle_path
from sugar.presence import presenceservice
+from sugar.datastore import datastore
bundlepath = get_bundle_path()
presenceService = presenceservice.get_instance()
@@ -92,10 +102,10 @@ class MazeGame:
canvas_size = screen.get_size()
self.aspectRatio = canvas_size[0] / float(canvas_size[1])
- # start with a small maze using a seed that will be different
- # each time you play
- self.maze = Maze(int(time.time()), int(9 * self.aspectRatio), 9)
- self.reset()
+ # no data is known yet (maybe the user is resuming a state or
+ # launching a new instance)
+ self.data = None
+
self.frame = 0
self.font = pygame.font.Font(None, 30)
@@ -283,8 +293,42 @@ class MazeGame:
(event, sys.exc_info())
else:
print "Message from unknown buddy?"
+
+ elif event.type == pygame.USEREVENT:
+ # process fil save / restore events
+ if event.code == olpcgames.FILE_READ_REQUEST:
+ log.debug(event)
+ log.debug('Loading the state of the game...')
+ self.data = simplejson.loads(open(event.filename, 'r').read())
+ log.debug('Loaded data: %s' % self.data)
+ return True
+ elif event.code == olpcgames.FILE_WRITE_REQUEST:
+ log.debug(event)
+ log.debug('Saving the state of the game...')
+ data = {'seed': self.maze.seed,
+ 'width': self.maze.width,
+ 'height': self.maze.height,
+ }
+ log.debug('Saving data: %s' % data)
+
+ file_dsobject = datastore.create()
+ file_dsobject.metadata = event.metadata
+
+ temp_filename = tempfile.mktemp()
+ f = open(temp_filename, 'w')
+ try:
+ f.write(simplejson.dumps(data))
+ finally:
+ f.close()
+
+ shutil.copyfile(temp_filename, event.filename)
+
+ file_dsobject.set_file_path(event.filename)
+ datastore.write(file_dsobject)
+ log.debug('Done saving.')
+ return True
else:
- print "Unknown event:", event
+ log.debug('Unknown event: %s' % event)
def handleMessage(self, player, message):
"""Handle a message from a player on the mesh.
@@ -357,9 +401,28 @@ class MazeGame:
def run(self):
"""Run the main loop of the game."""
# lets draw once before we enter the event loop
+
+ # We have to update the clock to get the first event because
+ # the game could be called from the Journal and we need to
+ # catch the olpcgames.FILE_READ_REQUEST event to load the last
+ # state of the game
+ clock = pygame.time.Clock()
+ clock.tick(25)
+ for event in pausescreen.get_events(sleep_timeout=30):
+ self.processEvent(event)
+
+ if self.data == None:
+ # start the game with default values (easy)
+ self.data = {'seed': int(time.time()),
+ 'width': int(9 * self.aspectRatio),
+ 'height': 9}
+
+ log.debug('Starting the game with: %s' % self.data)
+ self.maze = Maze(**self.data)
+ self.reset()
+
self.draw()
pygame.display.flip()
- clock = pygame.time.Clock()
while self.running:
a, b, c, d = pygame.cursors.load_xbm('my_cursor.xbm',
diff --git a/olpcgames/__init__.py b/olpcgames/__init__.py
index 5fd28b7..504388c 100644
--- a/olpcgames/__init__.py
+++ b/olpcgames/__init__.py
@@ -1,6 +1,6 @@
-"""Wrapper/adaptation system for writing/porting PyGame games to OLPC/Sugar
+"""Wrapper/adaptation system for writing/porting Pygame games to OLPC/Sugar
-The wrapper system attempts to substitute various pieces of the PyGame
+The wrapper system attempts to substitute various pieces of the Pygame
implementation in order to make code written without knowledge of the
OLPC/Sugar environment run "naturally" under the GTK environment of
Sugar. It also provides some convenience mechanisms for dealing with
@@ -8,9 +8,9 @@ e.g. the Camera and Mesh Network system.
Considerations for Developers:
-PyGame programs running under OLPCGames will generally not have
+Pygame programs running under OLPCGames will generally not have
"hardware" surfaces, and will not be able to have a reduced-resolution
-full-screen view to optimise rendering. The PyGame code will run in
+full-screen view to optimise rendering. The Pygame code will run in
a secondary thread, with the main GTK UI running in the primary thread.
A third "mainloop" thread will occasionally be created to handle the
GStreamer interface to the camera.
@@ -22,21 +22,81 @@ Attributes of Note:
WIDGET -- PygameCanvas instance, a GTK widget with an embedded
socket object which is a proxy for the SDL window Pygame to which
pygame renders.
+
+ Constants: All event constants used by the package are defined at this
+ level. Note that eventually we will need to switch to using UserEvent
+ and making these values sub-types rather than top-level types.
+
+
+Pygame events at the Activity Level:
+
+ pygame.USEREVENT
+ code == olpcgames.FILE_READ_REQUEST
+ filename (unicode/string) -- filename from which to read
+ metadata (dictionary-like) -- mapping from key to string values
+
+ Note: due to a limitation in the Sugar API, the GTK event loop
+ will be *frozen* during this operation, as a result you cannot
+ make any DBUS or GTK calls, nor can you use GUI during the
+ call to provide input. That is, you have to process this event
+ synchronously.
+
+ code == olpcgames.FILE_WRITE_REQUEST
+ filename (unicode/string) -- file name to which to write
+ metadata (dictionary-like) -- mapping from key: value where all
+ values must (currently) be strings
+
+ Note: due to a limitation in the Sugar API, the GTK event loop
+ will be *frozen* during this operation, as a result you cannot
+ make any DBUS or GTK calls, nor can you use GUI during the
+ call to provide input. That is, you have to process this event
+ synchronously.
+
+see also the mesh and camera modules for more events.
+
+Deprecated:
+
+ This module includes the activity.PyGameActivity class currently,
+ this is a deprecated mechanism for accessing the activity class,
+ and uses the deprecated spelling (case) of the name. Use:
+
+ from olpcgames import activity
+
+ class MyActivity( activity.PygameActivity ):
+ ...
+
+ to define your PygameActivity subclass (note the case of the
+ spelling, which now matches Pygame's own spelling).
"""
-# XXX handle configurations that are not running under Sugar and
-# report proper errors to describe the problem, rather than letting the
-# particular errors propagate outward.
-# XXX allow use of a particular feature within the library without needing
-# to be running under sugar. e.g. allow importing mesh or camera without
-# needing to import the activity machinery.
-from olpcgames.canvas import *
+from olpcgames._version import __version__
+ACTIVITY = None
+widget = WIDGET = None
+
+# XXX problem here, we're filling up the entirety of the Pygame
+# event-set with just this small bit of functionality, obviously
+# Pygame is not intending for this kind of usage!
+(
+ CAMERA_LOAD, CAMERA_LOAD_FAIL,
+
+ CONNECT,PARTICIPANT_ADD,
+ PARTICIPANT_REMOVE,
+ MESSAGE_UNI,MESSAGE_MULTI,
+) = range( 25, 32 )
+
+# These events use UserEvent.code, eventually *all* events should be
+# delivered as UserEvent with code set to the values defined here...
+
+(
+ #NET_CONNECT, NET_PARTICIPANT_ADD,NET_PARTICIPANT_REMOVE,
+ #NET_MESSAGE_UNICAST, NET_MESSAGE_MULTICAST,
+ #CAMERA_LOAD, CAMERA_LOAD_FAIL,
+ FILE_READ_REQUEST, FILE_WRITE_REQUEST,
+) = range(
+ 2**16, 2**16+2,
+)
+
try:
- from olpcgames.activity import *
+ from olpcgames.activity import PygameActivity as PyGameActivity
except ImportError, err:
- PyGameActivity = None
-from olpcgames import camera
-from olpcgames import pangofont
-from olpcgames import mesh
+ PyGameActivity = None
-ACTIVITY = None
-widget = WIDGET = None
diff --git a/olpcgames/_cairoimage.py b/olpcgames/_cairoimage.py
index 302b916..3cfa22c 100644
--- a/olpcgames/_cairoimage.py
+++ b/olpcgames/_cairoimage.py
@@ -1,23 +1,38 @@
-"""Utility functions for cairo-specific operations"""
-import cairo, pygame, struct
+"""Utility functions for cairo-specific operations
+
+USE_BASE_ARRAY -- if False (default), uses numpy arrays,
+ currently this is the only version that works on 32-bit
+ machines.
+"""
+import pygame, struct, logging
big_endian = struct.pack( '=i', 1 ) == struct.pack( '>i', 1 )
+log = logging.getLogger( 'olpcgames._cairoimage' )
+##log.setLevel( logging.DEBUG )
+
+USE_BASE_ARRAY = False
+
def newContext( width, height ):
"""Create a new render-to-image context
width, height -- pixel dimensions to be rendered
- returns surface, context for rendering
+ Produces an ARGB format Cairo ImageSurface for
+ rendering your data into using rsvg, Cairo or Pango.
+
+ returns (ImageSurface, CairoContext) for rendering
"""
+ import cairo
csrf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
- return csrf, cairo.Context (csrf)
+ context = cairo.Context (csrf)
+ #log.info( 'Format (expect: %s): %s', cairo.FORMAT_ARGB32, csrf.get_format())
+ return csrf, context
def mangle_color(color):
"""Mange a colour depending on endian-ness, and swap-necessity
- This implementation has only been tested on an AMD64
- machine with a get_data implementation (rather than
- a get_data_as_rgba implementation).
+ Converts a 3 or 4 int (or float) value in the range 0-255 into a
+ 4-float value in the range 0.0-1.0
"""
r,g,b = color[:3]
if len(color) > 3:
@@ -31,23 +46,58 @@ def _fixColorBase( v ):
return max((0,min((v,255.0))))/255.0
def asImage( csrf ):
- """Get the pixels in csrf as a Pygame image"""
+ """Get the pixels in csrf as a Pygame image
+
+ Note that Pygame 1.7.1 on (Gentoo Linux) AMD64 is incorrectly
+ calculating the required size ARGB images, so this code will *not* work
+ on that platform with that version of the library. Pygame-ctypes
+ does work correctly there.
+
+ Note also that Pygame 1.7.1 is showing a strange colour rotation
+ bug on 32-bit platforms, such that ARGB mode cannot be used for
+ images there. Instead we have to do an expensive bit-shift operation
+ to produce an RGBA image from the ARGB native Cairo format.
+
+ Will raise a ValueError if passed a Null image (i.e. dimension of 0)
+
+ returns Pygame.Surface (image) with convert_alpha() called for it.
+ """
# Create and return a new Pygame Image derived from the Cairo Surface
format = 'ARGB'
if hasattr(csrf,'get_data'):
# more recent API, native-format, but have to (potentially) convert the format...
+ log.debug( 'Native-mode api (get_data)' )
data = csrf.get_data()
if not big_endian:
- import numpy
- data = numpy.frombuffer( data, 'I' ).astype( '>I4' ).tostring()
+ # we use array here because it's considerably lighter-weight
+ # to import than the numpy module
+ log.debug( 'Not big-endian, byte-swapping array' )
+ if USE_BASE_ARRAY:
+ import array
+ a = array.array( 'I' )
+ a.fromstring( data )
+ a.byteswap()
+ data = a.tostring()
+ else:
+ import numpy
+ n = numpy.fromstring( data, dtype='I' )
+ n = ((n & 0xff000000) >> 24 ) | ((n & 0x00ffffff) << 8 )
+ n = n.byteswap()
+ data = n.tostring()
+ format = 'RGBA'
else:
+ log.debug( 'Big-endian, array unchanged' )
data = str(data) # there's one copy
else:
# older api, not native, but we know what it is...
+ log.debug( 'Non-native mode api, explicitly RGBA' )
data = csrf.get_data_as_rgba()
data = str(data) # there's one copy
+ format = 'RGBA'
width, height = csrf.get_width(),csrf.get_height()
+
try:
+ log.info( 'Format = %s', format )
return pygame.image.fromstring(
data,
(width,height),
@@ -56,3 +106,30 @@ def asImage( csrf ):
except ValueError, err:
err.args += (len(data), (width,height), width*height*4,format )
raise
+
+if __name__ == "__main__":
+ import unittest
+ logging.basicConfig()
+ class Tests( unittest.TestCase ):
+ def test_colours( self ):
+ """Test that colours are correctly translated
+
+ If we draw a given colour in cairo, we want the same
+ colour to show up in Pygame, let's test that...
+ """
+ for sourceColour in [
+ (255,0,0, 255),
+ (0,255,0, 255),
+ (0,0,255, 255),
+ (255,255,0, 255),
+ (0,255,255,255),
+ (255,0,255,255),
+ ]:
+ csrf,cctx = newContext( 1,1 )
+ background = mangle_color( sourceColour )
+ cctx.set_source_rgba(*background)
+ cctx.paint()
+ img = asImage( csrf )
+ colour = img.get_at( (0,0))
+ assert colour == sourceColour, (sourceColour,mangle_color(sourceColour),colour)
+ unittest.main()
diff --git a/olpcgames/_gtkmain.py b/olpcgames/_gtkmain.py
new file mode 100644
index 0000000..33a6a83
--- /dev/null
+++ b/olpcgames/_gtkmain.py
@@ -0,0 +1,70 @@
+"""Support for GObject mainloop-requiring libraries when not inside GTK
+
+INITIALIZED -- whether we have a running gobject loop yet...
+LOOP_TRACKER -- if present, the manual gtk event loop used to
+ support gobject-based code running in a non-Gobject event loop
+
+Holder -- objects which can be held as attributes to keep the mainloop running
+"""
+import threading, logging
+log = logging.getLogger( 'olpcgames._gtkmain' )
+##log.setLevel( logging.DEBUG )
+
+INITIALIZED = False
+LOOP_TRACKER = None
+
+class _TrackLoop( object ):
+ """Tracks the number of open loops and stops when finished"""
+ count = 0
+ _mainloop = None
+ def increment( self ):
+ log.info( 'Increment from %s', self.count )
+ self.count += 1 # XXX race condition here?
+ if self.count == 1:
+ log.info( 'Creating GObject mainloop')
+ self.t_loop = threading.Thread(target=self.loop)
+ self.t_loop.setDaemon( True )
+ self.t_loop.start()
+ def decrement( self ):
+ log.info( 'Decrement from %s', self.count )
+ self.count -= 1
+ def loop( self ):
+ """Little thread loop that replicates the gtk mainloop"""
+ import gtk
+ while self.count >= 1:
+ log.debug( 'GTK loop restarting' )
+ while gtk.events_pending():
+ gtk.main_iteration()
+ log.debug( 'GTK loop exiting' )
+ try:
+ del self.t_loop
+ except AttributeError, err:
+ pass
+
+class Holder():
+ """Object which, while held, keeps the gtk mainloop running"""
+ def __init__( self ):
+ log.info( 'Beginning hold on GTK mainloop with Holder object' )
+ startGTK()
+ def __del__( self ):
+ log.info( 'Releasing hold on GTK mainloop with Holder object' )
+ stopGTK()
+
+def startGTK( ):
+ """GTK support is required here, process..."""
+ if not INITIALIZED:
+ init()
+ if LOOP_TRACKER:
+ LOOP_TRACKER.increment()
+def stopGTK( ):
+ """GTK support is no longer required, release"""
+ if LOOP_TRACKER:
+ LOOP_TRACKER.decrement()
+def init( ):
+ """Create a gobject mainloop in a sub-thread (you don't need to call this normally)"""
+ global INITIALIZED, LOOP_TRACKER
+ if not INITIALIZED:
+ if not LOOP_TRACKER:
+ LOOP_TRACKER = _TrackLoop()
+ INITIALIZED = True
+ return LOOP_TRACKER
diff --git a/olpcgames/_version.py b/olpcgames/_version.py
new file mode 100644
index 0000000..6a4e1db
--- /dev/null
+++ b/olpcgames/_version.py
@@ -0,0 +1,2 @@
+"""Module defining the current version of the library"""
+__version__ = '1.6'
diff --git a/olpcgames/activity.py b/olpcgames/activity.py
index 77687cd..538ba13 100644
--- a/olpcgames/activity.py
+++ b/olpcgames/activity.py
@@ -1,54 +1,73 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-"""Embeds the Canvas widget into a Sugar-specific Activity environment"""
+"""Embeds the Canvas widget into a Sugar-specific Activity environment
+The olpcgames.activity module encapsulates creation of a Pygame activity.
+Your Activity should inherit from this class. Simply setting some class
+attributes is all you need to do in a class inheriting from
+olpcgames.activity.PygameActivity in order to get Pygame to work.
+
+(The skeleton builder script creates this file automatically for you).
+
+Note:
+ You should not import pygame into your activity file, as the olpcgames
+ wrapper needs to be initialized before pygame is imported the first time.
+
+Example usage:
+
+ class PygameActivity(activity.Activity):
+ game_name = None
+ game_title = 'Pygame Game'
+ game_size = (units.grid_to_pixels(16),
+ units.grid_to_pixels(11))
+ pygame_mode = 'SDL'
+"""
import logging
-logging.root.setLevel(logging.WARN)
-log = logging.getLogger('olpcgames.activity')
-#log.setLevel( logging.INFO )
+logging.root.setLevel( logging.WARN )
+log = logging.getLogger( 'olpcgames.activity' )
+##log.setLevel( logging.DEBUG )
import pygtk
pygtk.require('2.0')
import gtk
import gtk.gdk
+import os
from sugar.activity import activity
from sugar.graphics import style
-from olpcgames.canvas import PyGameCanvas
+from olpcgames.canvas import PygameCanvas
from olpcgames import mesh, util
-__all__ = ['PyGameActivity']
+__all__ = ['PygameActivity']
-
-class PyGameActivity(activity.Activity):
-
- """PyGame-specific activity type, provides boilerplate toolbar, creates canvas
+class PygameActivity(activity.Activity):
+ """Pygame-specific activity type, provides boilerplate toolbar, creates canvas
Subclass Overrides:
game_name -- specifies a fully-qualified name for the game's main-loop
format like so:
'package.module:main'
- if not function name is provided, "main" is assumed.
- game_handler -- alternate specification via direct
- reference to a main-loop
- function
+ if no function name is provided, "main" is assumed.
+
+ game_handler -- DEPRECATED. alternate specification via direct
+ reference to a main-loop function.
game_size -- two-value tuple specifying the size of the display in pixels,
this is currently static, so once the window is created it cannot be
changed.
- If None, use the bulk of the screen for the PyGame surface based on
+ If None, use the bulk of the screen for the Pygame surface based on
the values reported by the gtk.gdk functions. Note that None is
*not* the default value.
game_title -- title to be displayed in the Sugar Shell UI
pygame_mode -- chooses the rendering engine used for handling the
- PyGame drawing mode, 'SDL' chooses the standard PyGame renderer,
+ Pygame drawing mode, 'SDL' chooses the standard Pygame renderer,
'Cairo' chooses the experimental pygamecairo renderer.
+
+ Note: You likely do *not* want to use Cairo, it is no longer maintained.
- PYGAME_CANVAS_CLASS -- normally PyGameCanvas, but can be overridden
+ PYGAME_CANVAS_CLASS -- normally PygameCanvas, but can be overridden
if you want to provide a different canvas class, e.g. to provide a different
internal layout. Note: only used where pygame_mode == 'SDL'
@@ -60,138 +79,94 @@ class PyGameActivity(activity.Activity):
this super-class, with no easy way of overriding without completely rewriting
the __init__ method. We should allow for customising both the UI layout and
the toolbar contents/layout/connection.
-
+
XXX Note that if you change the title of your activity in the toolbar you may
see the same focus issues as we have patched around in the build_toolbar
method. If so, please report them to Mike Fletcher.
-
"""
-
game_name = None
- game_title = 'PyGame Game'
+ game_title = 'Pygame Game'
game_handler = None
- game_size = (16 * style.GRID_CELL_SIZE, 11 * style.GRID_CELL_SIZE)
+ game_size = (16 * style.GRID_CELL_SIZE,
+ 11 * style.GRID_CELL_SIZE)
pygame_mode = 'SDL'
def __init__(self, handle):
"""Initialise the Activity with the activity-description handle"""
-
- super(PyGameActivity, self).__init__(handle)
+ super(PygameActivity, self).__init__(handle)
self.make_global()
if self.game_size is None:
- (width, height) = (gtk.gdk.screen_width(),
- gtk.gdk.screen_height())
- log.info('Total screen size: %s %s', width, height)
-
+ width,height = gtk.gdk.screen_width(), gtk.gdk.screen_height()
+ log.info( 'Total screen size: %s %s', width,height)
# for now just fudge the toolbar size...
-
- self.game_size = (width, height - 1 * style.GRID_CELL_SIZE)
+ self.game_size = width, height - (1*style.GRID_CELL_SIZE)
self.set_title(self.game_title)
toolbar = self.build_toolbar()
- log.debug('Toolbar size: %s', toolbar.get_size_request())
+ log.debug( 'Toolbar size: %s', toolbar.get_size_request())
canvas = self.build_canvas()
+ self.connect( 'configure-event', canvas._translator.do_resize_event )
- def make_global(self):
+ def make_global( self ):
"""Hack to make olpcgames.ACTIVITY point to us
"""
+ import weakref, olpcgames
+ assert not olpcgames.ACTIVITY, """Activity.make_global called twice, have you created two Activity instances in a single process?"""
+ olpcgames.ACTIVITY = weakref.proxy( self )
- import weakref
- import olpcgames
- assert not olpcgames.ACTIVITY, \
- """Activity.make_global called twice, have you created two Activity instances in a single process?"""
- olpcgames.ACTIVITY = weakref.proxy(self)
-
- def build_toolbar(self):
+ def build_toolbar( self ):
"""Build our Activity toolbar for the Sugar system
This is a customisation point for those games which want to
provide custom toolbars when running under Sugar.
"""
-
- try:
- from sugar.graphics.toolbarbox import ToolbarBox, ToolbarButton
- from sugar.activity.widgets import ActivityToolbarButton, StopButton, \
- ShareButton
- from mybutton import MyActivityToolbarButton
-
- toolbar_box = ToolbarBox()
- activity_button = MyActivityToolbarButton(self)
- toolbar_box.toolbar.insert(activity_button, 0)
- activity_button.show()
-
- separator = gtk.SeparatorToolItem()
- separator.props.draw = False
- separator.set_expand(True)
- toolbar_box.toolbar.insert(separator, -1)
- separator.show()
-
- share_button = ShareButton(self)
- toolbar_box.toolbar.insert(share_button, -1)
- share_button.show()
-
- stop_button = StopButton(self)
- toolbar_box.toolbar.insert(stop_button, -1)
- stop_button.show()
-
- self.set_toolbar_box(toolbar_box)
- toolbar_box.show()
- toolbar=toolbar_box.toolbar
- except ImportError:
- toolbar = activity.ActivityToolbar(self)
- toolbar.show()
- self.set_toolbox(toolbar)
- toolbar.title.unset_flags(gtk.CAN_FOCUS)
-
+ toolbar = activity.ActivityToolbar(self)
+ toolbar.show()
+ self.set_toolbox(toolbar)
def shared_cb(*args, **kwargs):
- log.info('shared: %s, %s', args, kwargs)
+ log.info( 'shared: %s, %s', args, kwargs )
try:
mesh.activity_shared(self)
except Exception, err:
- log.error("""Failure signaling activity sharing to mesh module: %s"""
- , util.get_traceback(err))
+ log.error( """Failure signaling activity sharing to mesh module: %s""", util.get_traceback(err) )
else:
- log.info('mesh activity shared message sent, trying to grab focus'
- )
+ log.info( 'mesh activity shared message sent, trying to grab focus' )
try:
self._pgc.grab_focus()
except Exception, err:
- log.warn('Focus failed: %s', err)
+ log.warn( 'Focus failed: %s', err )
else:
- log.info('asserting focus')
- assert self._pgc.is_focus(), \
- """Did not successfully set pygame canvas focus"""
- log.info('callback finished')
-
+ log.info( 'asserting focus' )
+ assert self._pgc.is_focus(), """Did not successfully set pygame canvas focus"""
+ log.info( 'callback finished' )
+
def joined_cb(*args, **kwargs):
- log.info('joined: %s, %s', args, kwargs)
+ log.info( 'joined: %s, %s', args, kwargs )
mesh.activity_joined(self)
self._pgc.grab_focus()
-
- self.connect('shared', shared_cb)
- self.connect('joined', joined_cb)
+ self.connect("shared", shared_cb)
+ self.connect("joined", joined_cb)
if self.get_shared():
-
# if set at this point, it means we've already joined (i.e.,
# launched from Neighborhood)
-
joined_cb()
+ toolbar.title.unset_flags(gtk.CAN_FOCUS)
return toolbar
- PYGAME_CANVAS_CLASS = PyGameCanvas
-
- def build_canvas(self):
- """Construct the PyGame or PyGameCairo canvas for drawing"""
-
- assert self.game_handler or self.game_name, \
- 'You must specify a game_handler or game_name on your Activity (%r)' \
- % (self.game_handler or self.game_name)
+ PYGAME_CANVAS_CLASS = PygameCanvas
+ def build_canvas( self ):
+ """Construct the Pygame or PygameCairo canvas for drawing"""
+ assert self.game_handler or self.game_name, 'You must specify a game_handler or game_name on your Activity (%r)'%(
+ self.game_handler or self.game_name
+ )
if self.pygame_mode != 'Cairo':
self._pgc = self.PYGAME_CANVAS_CLASS(*self.game_size)
self.set_canvas(self._pgc)
self._pgc.grab_focus()
self._pgc.connect_game(self.game_handler or self.game_name)
+ # XXX Bad coder, do not hide in a widely subclassed operation
+ # map signal does not appear to show up on socket instances
gtk.gdk.threads_init()
return self._pgc
else:
@@ -209,7 +184,58 @@ class PyGameActivity(activity.Activity):
app = self.game_handler or self.game_name
if ':' not in app:
app += ':main'
- (mod_name, fn_name) = app.split(':')
+ mod_name, fn_name = app.split(':')
mod = __import__(mod_name, globals(), locals(), [])
fn = getattr(mod, fn_name)
fn()
+ def read_file(self, file_path):
+ """Handle request to read the given file on the Pygame side
+
+ This is complicated rather noticeably by the silly semantics of the Journal
+ where it unlinks the file as soon as this method returns. We either have to
+ handle the file-opening in PyGTK (not acceptable), block this thread until
+ the Pygame thread handles the event (which it may never do) or we have
+ to make the silly thing use a non-standard file-opening interface.
+ """
+ log.info( 'read_file: %s %s', file_path, self.metadata )
+ import olpcgames, pygame
+ from olpcgames import eventwrap
+ event = eventwrap.Event(
+ type = pygame.USEREVENT,
+ code = olpcgames.FILE_READ_REQUEST,
+ filename = file_path,
+ metadata = self.metadata,
+ )
+ eventwrap.post( event )
+ event.block()
+ def write_file( self, file_path ):
+ """Handle request to write to the given file on the Pygame side
+
+ This is rather complicated by the need to have the file complete by the
+ time the function returns. Very poor API, after all, if I have to write a
+ multi-hundred-megabyte file it might take many minutes to complete
+ writing.
+ """
+ log.info( 'write_file: %s %s', file_path, self.metadata )
+ if os.path.exists( file_path ):
+ self.read_file( file_path )
+ import olpcgames, pygame
+ from olpcgames import eventwrap
+ event = eventwrap.Event(
+ type = pygame.USEREVENT,
+ code = olpcgames.FILE_WRITE_REQUEST,
+ filename = file_path,
+ metadata = self.metadata,
+ )
+ eventwrap.post( event )
+ event.block()
+ if not os.path.exists( file_path ):
+ log.warn( '''No file created in %r''', file_path )
+ raise NotImplementedError( """Pygame Activity code did not produce a file for %s"""%( file_path, ))
+ else:
+ log.info( '''Stored file in %r''', file_path )
+
+
+import olpcgames
+olpcgames.PyGameActivity = PygameActivity
+PyGameActivity = PygameActivity
diff --git a/olpcgames/buildmanifest.py b/olpcgames/buildmanifest.py
new file mode 100755
index 0000000..899433b
--- /dev/null
+++ b/olpcgames/buildmanifest.py
@@ -0,0 +1,33 @@
+#! /usr/bin/env python
+"""Stupid little script to automate generation of MANIFEST and po/POTFILES.in
+
+Really this should have been handled by using distutils, but oh well,
+distutils is a hoary beast and I can't fault people for not wanting to
+spend days spelunking around inside it to find the solutions...
+"""
+from distutils.filelist import FileList
+import os
+
+def fileList( template ):
+ """Produce a formatted file-list for storing in a file"""
+ files = FileList()
+ for line in filter(None,template.splitlines()):
+ files.process_template_line( line )
+ content = '\n'.join( files.files )
+ return content
+
+
+def main( ):
+ """Do the quicky finding of files for our manifests"""
+ content = fileList( open('MANIFEST.in').read() )
+ open( 'MANIFEST','w').write( content )
+
+ content = fileList( open('POTFILES.in').read() )
+ try:
+ os.makedirs( 'po' )
+ except OSError, err:
+ pass
+ open( os.path.join('po','POTFILES.in'), 'w').write( content )
+
+if __name__ == "__main__":
+ main()
diff --git a/olpcgames/camera.py b/olpcgames/camera.py
index b51a394..249f295 100644
--- a/olpcgames/camera.py
+++ b/olpcgames/camera.py
@@ -2,38 +2,27 @@
Depends upon:
pygame
- python-gstreamer
+ gstreamer (particularly gst-launch)
+
+Activity demonstrating usage:
+
+ http://dev.laptop.org/git?p=projects/games-misc;a=tree;f=cameratest.activity;hb=HEAD
+
+
"""
-import threading
+import threading, subprocess
import logging
+import olpcgames
import time
import os
import pygame
-import gst
from olpcgames.util import get_activity_root
log = logging.getLogger( 'olpcgames.camera' )
#log.setLevel( logging.DEBUG )
-CAMERA_LOAD = 9917
-CAMERA_LOAD_FAIL = 9918
+CAMERA_LOAD, CAMERA_LOAD_FAIL = olpcgames.CAMERA_LOAD, olpcgames.CAMERA_LOAD
-class CameraSprite(object):
- """Create gstreamer surface for the camera."""
- def __init__(self, x, y):
- import olpcgames
- if olpcgames.WIDGET:
- self._init_video(olpcgames.WIDGET, x, y)
-
- def _init_video(self, widget, x, y):
- from olpcgames import video
- self._vid = video.VideoWidget()
- widget._fixed.put(self._vid, x, y)
- self._vid.show()
-
- self.player = video.Player(self._vid)
- self.player.play()
-
class Camera(object):
"""A class representing a still-picture camera
@@ -51,11 +40,18 @@ class Camera(object):
Note:
The Camera class is simply a convenience wrapper around a fairly
- straightforward gstreamer bus. If you have more involved
+ straightforward gst-launch bus. If you have more involved
requirements for your camera manipulations you will probably
find it easier to write your own camera implementation than to
use this one. Basically we provide here the "normal" use case of
snapping a picture into a pygame image.
+
+ Note:
+
+ With the current camera implementation taking a single photograph
+ requires about 6 seconds! Obviously we'll need to figure out what's
+ taking gstreamer so long to process the pipe and fix that.
+
"""
_aliases = {
'camera': 'v4l2src',
@@ -65,7 +61,7 @@ class Camera(object):
'jpeg': 'jpegenc',
'jpg': 'jpegenc',
}
- def __init__(self, source='camera', format='png', filename='snap.png', directory = None):
+ def __init__(self, source='camera', format='png', filename=None, directory = None):
"""Initialises the Camera's internal description
source -- the gstreamer source for the video to capture, useful values:
@@ -74,17 +70,22 @@ class Camera(object):
format -- the gstreamer encoder to use for the capture, useful values:
'pngenc','png' -- PNG format graphic
'jpegenc','jpg','jpeg' -- JPEG format graphic
- filename -- the filename to use for the capture
+ filename -- the filename to use for the capture, if not specified defaults
+ to a random UUID + '.' + format
directory -- the directory in which to create the temporary file, defaults
to get_activity_root() + 'tmp'
"""
+ log.info( 'Creating camera' )
+ if not filename:
+ import uuid
+ filename = '%s.%s'%( uuid.uuid4(), format )
self.source = self._aliases.get( source, source )
self.format = self._aliases.get( format, format )
self.filename = filename
self.directory = directory
- SNAP_PIPELINE = '%(source)s ! ffmpegcolorspace ! %(format)s ! filesink location="%(filename)s"'
- def _create_pipe( self ):
- """Method to create the cstreamer pipe from our settings"""
+ SNAP_PIPELINE = 'gst-launch','%(source)s','!','ffmpegcolorspace','!','%(format)s','!','filesink','location="%(filename)s"'
+ def _create_subprocess( self ):
+ """Method to create the gstreamer subprocess from our settings"""
if not self.directory:
path = os.path.join( get_activity_root(), 'tmp' )
try:
@@ -97,35 +98,27 @@ class Camera(object):
filename = os.path.join( path, self.filename )
format = self.format
source = self.source
- pipeline = self.SNAP_PIPELINE % locals()
- log.debug( 'Background thread processing: %s', pipeline )
- return filename, gst.parse_launch(pipeline)
-
+ pipeline = [s%locals() for s in self.SNAP_PIPELINE ]
+ return filename, subprocess.Popen(
+ pipeline,stderr = subprocess.PIPE
+ )
+
def snap(self):
"""Snap a picture via the camera by iterating gstreamer until finished
Note: this is an unsafe implementation, it will cause the whole
- activity to hang if the operation happens to fail! It is strongly
- recommended that you use snap_async instead of snap!
+ activity to hang until the capture finishes. Time to finish is often
+ measured in whole seconds (3-6s).
+
+ It is *strongly* recommended that you use snap_async instead of snap!
"""
log.debug( 'Starting snap' )
- filename, pipe = self._create_pipe()
- pipe.set_state(gst.STATE_PLAYING)
- bus = pipe.get_bus()
- tmp = False
- while True:
- event = self.bus.poll(gst.MESSAGE_STATE_CHANGED, 5)
- if event:
- old, new, pending = event.parse_state_changed()
- if pending == gst.STATE_VOID_PENDING:
- if tmp:
- break
- else:
- tmp = True
- else:
- break
- log.log( 'Ending snap, loading: %s', filename )
- return self._load_and_clean( filename )
+ filename, pipe = self._create_subprocess()
+ if not pipe.wait():
+ log.debug( 'Ending snap, loading: %s', filename )
+ return self._load_and_clean( filename )
+ else:
+ raise IOError( """Unable to complete snapshot: %s""", pipe.stderr.read() )
def _load_and_clean( self, filename ):
"""Use pygame to load given filename, delete after loading/attempt"""
try:
@@ -142,23 +135,33 @@ class Camera(object):
token -- passed back as attribute of the event which signals that capture
is finished
- We return two types of events CAMERA_LOAD and CAMERA_LOAD_FAIL,
+ We return events of type CAMERA_LOAD with an attribute "succeed"
depending on whether we succeed or not. Attributes of the events which
are returned:
+ success -- whether the loading process succeeded
token -- as passed to this method
- filename -- the filename in our temporary directory we used to store
- the file temporarily
image -- pygame image.load result if successful, None otherwise
+ filename -- the filename in our temporary directory we used to store
+ the file temporarily (this file will be deleted before the event
+ is sent, the name is for informational purposes only).
err -- Exception instance if failed, None otherwise
Basically identical to the snap method, save that it posts a message
- to the event bus in eventwrap instead of blocking and returning...
+ to the event bus in pygame.event instead of blocking and returning...
+
+ Example:
+ if event == pygame.MOUSEBUTTONDOWN:
+ camera = Camera( source='test', filename = 'picture32' )
+ camera.snap_async( myIdentifier )
+ ...
+ elif event.type == olpcgames.CAMERA_LOAD:
+ if event.token == myIdentifier:
+ doSomething( event.image )
"""
log.debug( 'beginning async snap')
- t = threading.Thread(target=self._background_snap, args=(token,))
+ t = threading.Thread(target=self._background_snap, args=[token])
t.start()
- log.debug( 'background thread started for gstreamer' )
return token
def _background_snap(
@@ -175,50 +178,33 @@ class Camera(object):
we begin playing, the second for when we finish.
"""
log.debug( 'Background thread kicking off gstreamer capture begun' )
- from olpcgames import eventwrap
- from pygame.event import Event
- filename, pipe = self._create_pipe()
- bus = pipe.get_bus()
- bus.add_signal_watch()
- def _background_snap_onmessage( bus, message ):
- """Handle messages from the picture-snapping bus"""
- log.debug( 'Message handler for gst messages: %s', message )
- t = message.type
- if t == gst.MESSAGE_EOS:
- pipe.set_state(gst.STATE_NULL)
- try:
- image = self._load_and_clean( filename )
- success = True
- except Exception, err:
- success = False
- image = None
- else:
- err = None
- log.debug( 'Success loading file %r', token )
- eventwrap.post(Event(
- CAMERA_LOAD,
- filename=filename,
- success = success,
- token = token,
- image=image,
- err=err
- ))
-
- elif t == gst.MESSAGE_ERROR:
- log.warn( 'Failure loading file %r: %s', token, message )
- pipe.set_state(gst.STATE_NULL)
- err, debug = message.parse_error()
- eventwrap.post(Event(
- CAMERA_LOAD_FAIL,
- filename=filename,
- success = False,
- token = token,
- image=None,
- err=err
- ))
- return False
- bus.connect('message', _background_snap_onmessage)
- pipe.set_state(gst.STATE_PLAYING)
+ from pygame.event import Event, post
+ filename, pipe = self._create_subprocess()
+ if not pipe.wait():
+ success = True
+ log.debug( 'Ending capture, loading: %s', filename )
+ try:
+ image = self._load_and_clean( filename )
+ except Exception, err:
+ image = None
+ success = False
+ else:
+ err = None
+ else:
+ success = False
+ err = pipe.stderr.read()
+ image = None
+ evt = Event(
+ CAMERA_LOAD,
+ dict(
+ filename=filename,
+ success = success,
+ token = token,
+ image=image,
+ err=err
+ )
+ )
+ post( evt )
def snap():
"""Dump a snapshot from the camera to a pygame surface in background thread
diff --git a/olpcgames/canvas.py b/olpcgames/canvas.py
index 2eee2c9..2583827 100644
--- a/olpcgames/canvas.py
+++ b/olpcgames/canvas.py
@@ -1,9 +1,9 @@
-"""Implements bridge connection between Sugar/GTK and PyGame"""
+"""Implements bridge connection between Sugar/GTK and Pygame"""
import os
import sys
import logging
log = logging.getLogger( 'olpcgames.canvas' )
-#log.setLevel( logging.DEBUG )
+##log.setLevel( logging.DEBUG )
import threading
from pprint import pprint
@@ -15,29 +15,31 @@ import pygame
from olpcgames import gtkEvent, util
-__all__ = ['PyGameCanvas']
+__all__ = ['PygameCanvas']
-class PyGameCanvas(gtk.Layout):
- """Canvas providing bridge methods to run PyGame in GTK
+class PygameCanvas(gtk.Layout):
+ """Canvas providing bridge methods to run Pygame in GTK
- The PyGameCanvas creates a secondary thread in which the Pygame instance will
- live, providing synthetic PyGame events to that thread via a Queue. The GUI
- connection is done by having the PyGame canvas use a GTK Port object as it's
+ The PygameCanvas creates a secondary thread in which the Pygame instance will
+ live, providing synthetic Pygame events to that thread via a Queue. The GUI
+ connection is done by having the Pygame canvas use a GTK Port object as it's
window pointer, it draws to that X-level window in order to produce output.
"""
+ mod_name = None
def __init__(self, width, height):
"""Initializes the Canvas Object
width,height -- passed to the inner EventBox in order to request a given size,
- the Socket is the only child of this EventBox, and the PyGame commands
+ the Socket is the only child of this EventBox, and the Pygame commands
will be writing to the Window ID of the socket. The internal EventBox is
- centered via an Alignment instance within the PyGameCanvas instance.
+ centered via an Alignment instance within the PygameCanvas instance.
XXX Should refactor so that the internal setup can be controlled by the
sub-class, e.g. to get size from the host window, or something similar.
"""
# Build the main widget
- super(PyGameCanvas,self).__init__()
+ log.info( 'Creating the pygame canvas' )
+ super(PygameCanvas,self).__init__()
self.set_flags(gtk.CAN_FOCUS)
# Build the sub-widgets
@@ -57,7 +59,7 @@ class PyGameCanvas(gtk.Layout):
self._align.show()
- self.put(self._align, 0, 0)
+ self.put(self._align, 0,0)
# Construct a gtkEvent.Translator
self._translator = gtkEvent.Translator(self, self._inner_evb)
@@ -73,10 +75,11 @@ class PyGameCanvas(gtk.Layout):
Side effects:
Sets the SDL_WINDOWID variable to our socket's window ID
- Calls PyGame init
- Causes the gtkEvent.Translator to "hook" PyGame
- Creates and starts secondary thread for Game/PyGame event processing.
+ Calls Pygame init
+ Causes the gtkEvent.Translator to "hook" Pygame
+ Creates and starts secondary thread for Game/Pygame event processing.
"""
+ log.info( 'Connecting the pygame canvas' )
# Setup the embedding
os.environ['SDL_WINDOWID'] = str(self._socket.get_id())
#print 'Socket ID=%s'%os.environ['SDL_WINDOWID']
@@ -87,21 +90,23 @@ class PyGameCanvas(gtk.Layout):
# Load the modules
# NOTE: This is delayed because pygame.init() must come after the embedding is up
if ':' not in app:
- app += ':main'
+ app += ':main'
mod_name, fn_name = app.split(':')
+ self.mod_name = mod_name
mod = __import__(mod_name, globals(), locals(), [])
fn = getattr(mod, fn_name)
+
# Start Pygame
self.__thread = threading.Thread(target=self._start, args=[fn])
self.__thread.start()
def _start(self, fn):
"""The method that actually runs in the background thread"""
+ log.info( 'Staring the mainloop' )
import olpcgames
olpcgames.widget = olpcgames.WIDGET = self
try:
- import sugar.activity.activity
- import os
+ import sugar.activity.activity,os
except ImportError, err:
log.info( """Running outside Sugar""" )
else:
@@ -112,13 +117,55 @@ class PyGameCanvas(gtk.Layout):
try:
try:
- log.info( '''Running mainloop: %s''', fn )
- fn()
- except Exception, err:
- log.error(
- """Uncaught top-level exception: %s""",
- util.get_traceback( err ),
- )
- raise
+ try:
+ log.info( '''Running mainloop: %s''', fn )
+ fn()
+ except Exception, err:
+ log.error(
+ """Uncaught top-level exception: %s""",
+ util.get_traceback( err ),
+ )
+ raise
+ else:
+ log.info( "Mainloop exited" )
+ finally:
+ log.debug( "Clearing any pending events" )
+ from olpcgames import eventwrap
+ eventwrap.clear()
finally:
+ log.info( 'Main function finished, calling main_quit' )
gtk.main_quit()
+
+ source_object_id = None
+ def view_source(self):
+ """Implement the 'view source' key by saving
+ datastore, and then telling the Journal to view it."""
+ if self.source_object_id is None:
+ from sugar import profile
+ from sugar.datastore import datastore
+ from sugar.activity.activity import get_bundle_name, get_bundle_path
+ from gettext import gettext as _
+ import os.path
+ jobject = datastore.create()
+ metadata = {
+ 'title': _('%s Source') % get_bundle_name(),
+ 'title_set_by_user': '1',
+ 'suggested_filename': 'pippy_app.py',
+ 'icon-color': profile.get_color().to_string(),
+ 'mime_type': 'text/x-python',
+ }
+ for k,v in metadata.items():
+ jobject.metadata[k] = v # dict.update method is missing =(
+ jobject.file_path = os.path.join(get_bundle_path(), 'pippy_app.py')
+ datastore.write(jobject)
+ self.__source_object_id = jobject.object_id
+ jobject.destroy()
+ self.journal_show_object(self.__source_object_id)
+ def journal_show_object(self, object_id):
+ """Invoke journal_show_object from sugar.activity.activity if it
+ exists."""
+ try:
+ from sugar.activity.activity import show_object_in_journal
+ show_object_in_journal(object_id)
+ except ImportError:
+ pass # no love from sugar.
diff --git a/olpcgames/data/sleeping_svg.py b/olpcgames/data/sleeping_svg.py
index fa67eee..c52398a 100644
--- a/olpcgames/data/sleeping_svg.py
+++ b/olpcgames/data/sleeping_svg.py
@@ -3,499 +3,59 @@
# written by resourcepackage: (1, 0, 1)
source = 'sleeping.svg'
package = 'olpcgames.data'
-data = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\012<!-- \
-Created with Inkscape (http://www.inkscape.org/) -->\012<svg\012 \
- xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\012 xmlns:cc=\"ht\
-tp://web.resource.org/cc/\"\012 xmlns:rdf=\"http://www.w3.org/1\
-999/02/22-rdf-syntax-ns#\"\012 xmlns:svg=\"http://www.w3.org/20\
-00/svg\"\012 xmlns=\"http://www.w3.org/2000/svg\"\012 xmlns:xlink\
-=\"http://www.w3.org/1999/xlink\"\012 xmlns:sodipodi=\"http://so\
-dipodi.sourceforge.net/DTD/sodipodi-0.dtd\"\012 xmlns:inkscape\
-=\"http://www.inkscape.org/namespaces/inkscape\"\012 width=\"736\
-.60107\"\012 height=\"923.09717\"\012 id=\"svg2\"\012 sodipodi:versi\
-on=\"0.32\"\012 inkscape:version=\"0.45.1\"\012 sodipodi:docbase=\"\
-/home/mcfletch/olpc/code/games-misc/olpcgames-src/olpcgames/\
-data\"\012 sodipodi:docname=\"sleeping.svg\"\012 inkscape:output_\
-extension=\"org.inkscape.output.svg.inkscape\"\012 version=\"1.0\
-\">\012 <defs\012 id=\"defs4\">\012 <linearGradient\012 id=\"l\
-inearGradient3152\">\012 <stop\012 style=\"stop-color:#\
-b8ffb4;stop-opacity:1;\"\012 offset=\"0\"\012 id=\"sto\
-p3154\" />\012 <stop\012 id=\"stop3214\"\012 offset\
-=\"0.5\"\012 style=\"stop-color:#2eff22;stop-opacity:0.498\
-03922;\" />\012 <stop\012 style=\"stop-color:#ffffff;st\
-op-opacity:0;\"\012 offset=\"1\"\012 id=\"stop3156\" />\
-\012 </linearGradient>\012 <radialGradient\012 inkscape:c\
-ollect=\"always\"\012 xlink:href=\"#linearGradient3152\"\012 \
- id=\"radialGradient3158\"\012 cx=\"260.12256\"\012 cy=\"\
-235.24702\"\012 fx=\"260.12256\"\012 fy=\"235.24702\"\012 \
- r=\"259.29678\"\012 gradientTransform=\"matrix(1,0,0,1.253\
-1846,0,-59.560934)\"\012 gradientUnits=\"userSpaceOnUse\" />\
-\012 </defs>\012 <sodipodi:namedview\012 id=\"base\"\012 pagecol\
-or=\"#ffffff\"\012 bordercolor=\"#666666\"\012 borderopacity=\"\
-1.0\"\012 gridtolerance=\"10000\"\012 guidetolerance=\"10\"\012 \
- objecttolerance=\"10\"\012 inkscape:pageopacity=\"0.0\"\012 \
-inkscape:pageshadow=\"2\"\012 inkscape:zoom=\"1.2109676\"\012 \
-inkscape:cx=\"382.85714\"\012 inkscape:cy=\"461.2069\"\012 ink\
-scape:document-units=\"px\"\012 inkscape:current-layer=\"layer\
-1\"\012 inkscape:window-width=\"1600\"\012 inkscape:window-he\
-ight=\"1127\"\012 inkscape:window-x=\"0\"\012 inkscape:window-\
-y=\"0\" />\012 <metadata\012 id=\"metadata7\">\012 <rdf:RDF>\012 \
- <cc:Work\012 rdf:about=\"\">\012 <dc:format>image/s\
-vg+xml</dc:format>\012 <dc:type\012 rdf:resource=\
-\"http://purl.org/dc/dcmitype/StillImage\" />\012 </cc:Work>\
-\012 </rdf:RDF>\012 </metadata>\012 <g\012 inkscape:label=\"Laye\
-r 1\"\012 inkscape:groupmode=\"layer\"\012 id=\"layer1\"\012 t\
-ransform=\"translate(-3.6769901,-73.051161)\">\012 <path\012 \
- sodipodi:type=\"arc\"\012 style=\"opacity:1;color:#000000;\
-fill:url(#radialGradient3158);fill-opacity:1;fill-rule:eveno\
-dd;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-l\
-inejoin:miter;marker:none;marker-start:none;marker-mid:none;\
-marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;st\
-roke-dashoffset:0;stroke-opacity:1;visibility:visible;displa\
-y:inline;overflow:visible\"\012 id=\"path2178\"\012 sodip\
-odi:cx=\"260.12256\"\012 sodipodi:cy=\"235.24702\"\012 sod\
-ipodi:rx=\"259.29678\"\012 sodipodi:ry=\"324.94675\"\012 d\
-=\"M 519.41934 235.24702 A 259.29678 324.94675 0 1 1 0.82577\
-515,235.24702 A 259.29678 324.94675 0 1 1 519.41934 235.247\
-02 z\"\012 transform=\"matrix(1.4203822,0,0,1.4203822,2.504\
-0738,200.45905)\" />\012 <path\012 style=\"fill:#000000;fil\
-l-opacity:0.75;fill-rule:nonzero;stroke:none;stroke-width:1p\
-t;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\
-\"\012 id=\"path2160\"\012 d=\"M 420.07983,366.13071 C 420\
-.35487,366.47717 420.72076,366.7246 421.0413,367.02162 C 421\
-.54375,367.47276 422.06281,367.90429 422.57382,368.34583 C 4\
-23.28729,368.97986 424.02228,369.58903 424.74649,370.2106 C \
-425.62503,370.96294 426.49869,371.72131 427.37129,372.48063 \
-C 428.38549,373.33736 429.37161,374.22678 430.36963,375.1020\
-2 C 431.51957,376.11144 432.66227,377.12909 433.8068,378.144\
-7 C 435.10375,379.28207 436.39638,380.42416 437.68916,381.56\
-629 C 439.07238,382.75868 440.43413,383.9754 441.79974,385.1\
-8789 C 443.20004,386.42238 444.62683,387.62616 446.04562,388\
-.8391 C 447.49676,390.05005 448.96429,391.24028 450.43445,39\
-2.42792 C 451.96417,393.65117 453.55907,394.78949 455.13698,\
-395.94898 C 456.83948,397.17768 458.60045,398.31988 460.3532\
-5,399.47417 C 462.27994,400.72959 464.31105,401.80854 466.32\
-055,402.92156 C 468.55661,404.14536 470.8622,405.23086 473.1\
-6967,406.31074 C 475.67184,407.45821 478.22515,408.48722 480\
-.77405,409.52454 C 483.51937,410.62204 486.31152,411.59223 4\
-89.10452,412.55924 C 492.05602,413.59241 495.06998,414.42774\
- 498.07923,415.2716 C 501.23779,416.16616 504.44361,416.8708\
-8 507.64836,417.5745 C 510.92868,418.27561 514.24385,418.790\
-77 517.55871,419.29543 C 520.86716,419.83295 524.19756,420.1\
-9662 527.5309,420.53139 C 530.86062,420.85145 534.19927,421.\
-03598 537.53901,421.20717 C 540.87406,421.39592 544.2147,421\
-.33746 547.55212,421.28446 C 550.92768,421.21773 554.29512,4\
-20.98306 557.66012,420.72502 C 561.0209,420.45787 564.3698,4\
-20.06437 567.71901,419.68331 C 570.96499,419.33121 574.18795\
-,418.8046 577.41097,418.29085 C 580.55689,417.7694 583.67686\
-,417.11029 586.79515,416.44738 C 589.93249,415.74608 593.044\
-28,414.93908 596.15094,414.1143 C 599.21068,413.29028 602.23\
-59,412.3538 605.25231,411.38586 C 608.26457,410.38752 611.22\
-46,409.24314 614.18464,408.1019 C 617.09398,406.98339 619.95\
-45,405.75011 622.80697,404.49587 C 625.61411,403.24034 628.3\
-8046,401.89814 631.14782,400.55819 C 633.83692,399.24273 636\
-.49752,397.87128 639.15408,396.49166 C 641.69701,395.16459 6\
-44.21761,393.79564 646.73927,392.4287 C 649.226,391.07864 65\
-1.69596,389.6984 654.16621,388.31856 C 664.28369,382.47714 6\
-35.39954,399.24448 647.39096,392.20906 C 649.71362,390.83128\
- 652.01714,389.42151 654.32347,388.01666 C 656.58074,386.649\
-11 658.81401,385.24283 661.04492,383.83292 C 663.187,382.479\
-81 665.31986,381.11219 667.45597,379.74972 L 682.67421,373.8\
-2852 C 680.53333,375.19377 678.39504,376.56299 676.25306,377\
-.92652 C 674.02228,379.3423 671.79047,380.75695 669.53525,38\
-2.13372 C 667.22445,383.53974 664.91761,384.95222 662.60433,\
-386.35423 C 655.61128,390.50089 648.56215,394.55382 641.4868\
-8,398.55869 C 639.01339,399.93149 636.53782,401.30047 634.04\
-976,402.64675 C 631.52675,404.00642 629.00428,405.36735 626.\
-45516,406.67776 C 623.79203,408.04539 621.12639,409.40838 61\
-8.42657,410.70277 C 615.65387,412.03449 612.87794,413.36009 \
-610.05858,414.59127 C 607.18935,415.82018 604.3116,417.02842\
- 601.39097,418.13174 C 598.41438,419.25931 595.43663,420.387\
-92 592.40026,421.34741 C 589.36654,422.28426 586.3267,423.20\
-206 583.25307,424.00155 C 580.12921,424.80704 577.00369,425.\
-61049 573.84621,426.27614 C 570.71325,426.91875 567.57887,42\
-7.5585 564.41815,428.0531 C 561.18267,428.54536 557.94654,42\
-9.03847 554.69048,429.37858 C 551.33145,429.74649 547.97234,\
-430.1215 544.60158,430.36782 C 541.21644,430.5953 537.82951,\
-430.81131 534.43558,430.8518 C 531.07392,430.87518 527.71049\
-,430.89848 524.35301,430.68741 C 520.99953,430.49414 517.647\
-13,430.28301 514.30415,429.94706 C 510.95344,429.57661 507.6\
-051,429.18426 504.27719,428.63828 C 500.94305,428.10693 497.\
-60828,427.56958 494.31089,426.83677 C 491.08361,426.10923 48\
-7.85646,425.37704 484.67505,424.46407 C 481.64095,423.59885 \
-478.60428,422.73682 475.62985,421.67916 C 472.81539,420.6957\
-9 470.00238,419.70689 467.23755,418.58909 C 464.66531,417.53\
-539 462.08915,416.48836 459.56863,415.31414 C 457.23636,414.\
-21006 454.9071,413.09837 452.64473,411.85467 C 450.60324,410\
-.7133 448.53944,409.60683 446.58754,408.31323 C 444.81432,40\
-7.13778 443.02862,405.98003 441.31079,404.72322 C 439.71016,\
-403.54798 438.09384,402.39203 436.55288,401.13812 C 435.0762\
-1,399.94004 433.59657,398.74568 432.14156,397.52114 C 430.71\
-41,396.29841 429.28064,395.08245 427.873,393.83679 C 426.503\
-83,392.62682 425.13537,391.41618 423.75477,390.21924 C 422.4\
-5847,389.08225 421.16119,387.9464 419.86302,386.81152 C 418.\
-70791,385.79925 417.55469,384.78482 416.40007,383.772 C 415.\
-40028,382.90611 414.40456,382.03536 413.39731,381.17806 C 41\
-2.52422,380.4317 411.656,379.67945 410.77279,378.9451 C 410.\
-03818,378.32256 409.30237,377.70143 408.56894,377.07747 C 40\
-8.05171,376.64287 407.52411,376.22075 407.01733,375.77379 C \
-406.66439,375.42865 406.27154,375.12044 405.95609,374.736 L \
-420.07983,366.13071 z \" />\012 <path\012 d=\"M 322.28988,3\
-66.13071 C 322.01483,366.47717 321.64895,366.7246 321.32841,\
-367.02162 C 320.82597,367.47276 320.3069,367.90429 319.79589\
-,368.34583 C 319.08241,368.97986 318.34743,369.58903 317.623\
-22,370.2106 C 316.74468,370.96294 315.87102,371.72131 314.99\
-842,372.48063 C 313.98423,373.33736 312.9981,374.22678 312.0\
-001,375.10202 C 310.85015,376.11144 309.70744,377.12909 308.\
-56293,378.1447 C 307.26598,379.28207 305.97336,380.42416 304\
-.68058,381.56629 C 303.29734,382.75868 301.93559,383.9754 30\
-0.57,385.18789 C 299.1697,386.42238 297.74291,387.62616 296.\
-32412,388.8391 C 294.873,390.05005 293.40545,391.24028 291.9\
-3531,392.42792 C 290.40559,393.65117 288.81068,394.78949 287\
-.23278,395.94898 C 285.53028,397.17768 283.7693,398.31988 28\
-2.01651,399.47417 C 280.08982,400.72959 278.0587,401.80854 2\
-76.0492,402.92156 C 273.81315,404.14536 271.50756,405.23086 \
-269.20009,406.31074 C 266.69792,407.45821 264.14461,408.4872\
-2 261.59571,409.52454 C 258.85039,410.62204 256.05823,411.59\
-223 253.26523,412.55924 C 250.31374,413.59241 247.29977,414.\
-42774 244.29052,415.2716 C 241.13196,416.16616 237.92614,416\
-.87088 234.72139,417.5745 C 231.44108,418.27561 228.12591,41\
-8.79077 224.81105,419.29543 C 221.50259,419.83295 218.17219,\
-420.19662 214.83886,420.53139 C 211.50914,420.85145 208.1704\
-9,421.03598 204.83074,421.20717 C 201.4957,421.39592 198.155\
-06,421.33746 194.81763,421.28446 C 191.44208,421.21773 188.0\
-7464,420.98306 184.70964,420.72502 C 181.34886,420.45787 177\
-.99995,420.06437 174.65075,419.68331 C 171.40476,419.33121 1\
-68.1818,418.8046 164.95878,418.29085 C 161.81286,417.7694 15\
-8.69291,417.11029 155.57462,416.44738 C 152.43728,415.74608 \
-149.3255,414.93908 146.21884,414.1143 C 143.1591,413.29028 1\
-40.13389,412.3538 137.1175,411.38586 C 134.10524,410.38752 1\
-31.14523,409.24314 128.1852,408.1019 C 125.27586,406.98339 1\
-22.41534,405.75011 119.56288,404.49587 C 116.75575,403.24034\
- 113.9894,401.89814 111.22207,400.55819 C 108.53297,399.2427\
-3 105.87237,397.87128 103.21581,396.49166 C 100.67287,395.16\
-459 98.152294,393.79564 95.630638,392.4287 C 93.14389,391.07\
-864 90.673951,389.6984 88.203707,388.31856 C 78.086243,382.4\
-7714 106.97036,399.24448 94.978939,392.20906 C 92.656273,390\
-.83128 90.352774,389.42151 88.046448,388.01666 C 85.789177,3\
-86.64911 83.555916,385.24283 81.324993,383.83292 C 79.182934\
-,382.47981 77.050074,381.11219 74.913953,379.74972 L 59.6957\
-31,373.82852 C 61.836611,375.19377 63.974911,376.56299 66.11\
-6884,377.92652 C 68.34766,379.3423 70.579478,380.75695 72.83\
-467,382.13372 C 75.145475,383.53974 77.452326,384.95222 79.7\
-65589,386.35423 C 86.758645,390.50089 93.807748,394.55382 10\
-0.883,398.55869 C 103.3565,399.93149 105.83208,401.30047 108\
-.32013,402.64675 C 110.84314,404.00642 113.36559,405.36735 1\
-15.9147,406.67776 C 118.57783,408.04539 121.24346,409.40838 \
-123.94327,410.70277 C 126.71597,412.03449 129.49189,413.3600\
-9 132.31123,414.59127 C 135.18046,415.82018 138.0582,417.028\
-42 140.97881,418.13174 C 143.95541,419.25931 146.93315,420.3\
-8792 149.96952,421.34741 C 153.00323,422.28426 156.04307,423\
-.20206 159.1167,424.00155 C 162.24054,424.80704 165.36606,42\
-5.61049 168.52355,426.27614 C 171.65651,426.91875 174.79088,\
-427.5585 177.9516,428.0531 C 181.18709,428.54536 184.42322,4\
-29.03847 187.67927,429.37858 C 191.03831,429.74649 194.39741\
-,430.1215 197.76818,430.36782 C 201.15332,430.5953 204.54025\
-,430.81131 207.93418,430.8518 C 211.29584,430.87518 214.6592\
-6,430.89848 218.01675,430.68741 C 221.37023,430.49414 224.72\
-263,430.28301 228.06561,429.94706 C 231.41632,429.57661 234.\
-76466,429.18426 238.09257,428.63828 C 241.4267,428.10693 244\
-.76148,427.56958 248.05887,426.83677 C 251.28615,426.10923 2\
-54.5133,425.37704 257.69471,424.46407 C 260.7288,423.59885 2\
-63.76548,422.73682 266.7399,421.67916 C 269.55436,420.69579 \
-272.36737,419.70689 275.1322,418.58909 C 277.70444,417.53539\
- 280.2806,416.48836 282.80113,415.31414 C 285.1334,414.21006\
- 287.46265,413.09837 289.72502,411.85467 C 291.7665,410.7133\
- 293.83031,409.60683 295.78222,408.31323 C 297.55542,407.137\
-78 299.34112,405.98003 301.05894,404.72322 C 302.65958,403.5\
-4798 304.27589,402.39203 305.81685,401.13812 C 307.29352,399\
-.94004 308.77316,398.74568 310.22816,397.52114 C 311.65561,3\
-96.29841 313.08907,395.08245 314.49672,393.83679 C 315.86588\
-,392.62682 317.23435,391.41618 318.61494,390.21924 C 319.911\
-24,389.08225 321.20852,387.9464 322.50669,386.81152 C 323.66\
-18,385.79925 324.81503,384.78482 325.96963,383.772 C 326.969\
-42,382.90611 327.96513,382.03536 328.97238,381.17806 C 329.8\
-4548,380.4317 330.71369,379.67945 331.59691,378.9451 C 332.3\
-3152,378.32256 333.06732,377.70143 333.80075,377.07747 C 334\
-.31799,376.64287 334.84558,376.22075 335.35236,375.77379 C 3\
-35.70529,375.42865 336.09814,375.12044 336.4136,374.736 L 32\
-2.28988,366.13071 z \"\012 id=\"path2162\"\012 style=\"fil\
-l:#000000;fill-opacity:0.75;fill-rule:nonzero;stroke:none;st\
-roke-width:1pt;stroke-linecap:butt;stroke-linejoin:miter;str\
-oke-opacity:1\" />\012 <path\012 style=\"fill:#000000;fill-\
-opacity:0.75;fill-rule:nonzero;stroke:none;stroke-width:1pt;\
-stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\"\012\
- id=\"path2174\"\012 d=\"M 363.56451,383.66406 C 363.2\
-9789,384.11947 363.11097,384.61437 362.88438,385.08913 C 362\
-.56503,385.71349 362.28415,386.35637 361.98417,386.98968 C 3\
-61.59883,387.80682 361.31001,388.66309 360.97303,389.49955 C\
- 360.58158,390.48782 360.2446,391.49591 359.88304,392.49501 \
-C 359.47472,393.65559 359.09862,394.82693 358.71289,395.9950\
-8 C 358.28054,397.30269 357.91308,398.63001 357.52373,399.95\
-051 C 357.10226,401.42131 356.77445,402.91584 356.41772,404.\
-40288 C 356.03758,406.05957 355.67624,407.72044 355.31143,40\
-9.38051 C 354.93639,411.10586 354.62209,412.8432 354.29778,4\
-14.57846 C 353.95914,416.41072 353.68547,418.25378 353.39986\
-,420.09471 C 353.09767,422.01995 352.87618,423.95563 352.643\
-78,425.88995 C 352.40586,427.8967 352.24593,429.91073 352.07\
-562,431.92382 C 351.91629,433.95529 351.78304,435.98864 351.\
-64542,438.02165 C 351.5051,440.1096 351.38765,442.19895 351.\
-26622,444.28805 C 351.13491,446.44906 350.9941,448.60949 350\
-.85664,450.77013 C 350.72765,452.93795 350.56813,455.10378 3\
-50.41467,457.26996 C 350.25248,459.46881 350.07615,461.66658\
- 349.90426,463.86466 C 349.72711,466.05242 349.50047,468.235\
-71 349.28474,470.41986 C 349.06933,472.63327 348.80288,474.8\
-409 348.54333,477.04935 C 348.27687,479.24544 347.9552,481.4\
-3401 347.6439,483.62388 C 347.3292,485.821 346.94529,488.006\
-52 346.56931,490.19356 C 346.18304,492.38223 345.7145,494.55\
-499 345.2634,496.73079 C 344.80304,498.88744 344.25754,501.0\
-2401 343.72541,503.16357 C 343.19873,505.27872 342.60709,507\
-.37642 342.02636,509.477 C 341.45242,511.5036 340.81692,513.\
-51179 340.19303,515.52327 C 339.579,517.48655 338.89351,519.\
-4259 338.22066,521.3693 C 337.52576,523.32558 336.75445,525.\
-25314 335.99664,527.18551 C 335.25621,529.09523 334.4656,530\
-.98465 333.68412,532.87762 C 332.89153,534.72375 332.09494,5\
-36.56812 331.29879,538.4127 C 330.52012,540.19102 329.72848,\
-541.9636 328.94044,543.73774 C 328.17627,545.48552 327.37923\
-,547.21844 326.58698,548.95351 C 325.79159,550.70985 324.963\
-38,552.45063 324.13807,554.19294 C 323.29657,555.94061 322.4\
-3736,557.67961 321.58006,559.41955 C 320.71339,561.15716 319\
-.85494,562.89885 318.99475,564.63967 C 318.12909,566.38264 3\
-17.29007,568.13858 316.44572,569.8919 C 315.57499,571.66105 \
-314.78167,573.46556 313.97771,575.26527 C 313.19325,577.0957\
-7 312.47033,578.95167 311.73461,580.80204 C 310.99762,582.66\
-364 310.33451,584.55312 309.65608,586.43649 C 308.96316,588.\
-34295 308.36923,590.28231 307.75868,592.21589 C 307.15712,59\
-4.14403 306.62912,596.09342 306.0891,598.03922 C 305.56263,5\
-99.9412 305.15523,601.87189 304.72869,603.79759 C 304.30073,\
-605.67457 304.06586,607.58362 303.80201,609.48748 C 303.5324\
-9,611.35505 303.50387,613.23908 303.43969,615.12014 C 303.37\
-464,616.94922 303.54021,618.77033 303.66927,620.59176 C 303.\
-79296,622.3199 304.08519,624.02622 304.35157,625.73538 C 304\
-.61502,627.39034 305.10873,628.99125 305.56074,630.60013 C 3\
-06.00752,632.15988 306.58838,633.67514 307.12864,635.20312 C\
- 307.6775,636.69384 308.43059,638.09344 309.11955,639.52034 \
-C 309.81034,640.91074 310.62616,642.2312 311.40271,643.57325\
- C 312.16936,644.91811 313.11662,646.14135 314.00418,647.403\
-93 C 314.99219,648.72656 316.15747,649.89632 317.28843,651.0\
-9288 C 318.46185,652.30686 319.74291,653.40671 321.00019,654\
-.53049 C 322.2966,655.6648 323.68994,656.67667 325.06765,657\
-.70741 C 326.47942,658.75983 327.95117,659.72705 329.40559,6\
-60.71847 C 330.83949,661.67678 332.34116,662.52443 333.83093\
-,663.39053 C 335.29556,664.25037 336.83732,664.96109 338.365\
-67,665.69625 C 339.90883,666.42997 341.52771,666.97576 343.1\
-3097,667.55664 C 344.73218,668.10981 346.35529,668.58875 347\
-.97795,669.07256 C 349.65491,669.51863 351.38306,669.68065 3\
-53.09946,669.88963 C 354.84867,670.06333 356.60545,670.12506\
- 358.36018,670.20906 C 360.07756,670.2966 361.79638,670.2728\
-7 363.51489,670.26883 C 365.20531,670.27174 366.89234,670.15\
-027 368.57919,670.0587 C 370.24527,669.9842 371.90054,669.78\
-813 373.55711,669.60806 C 375.19642,669.43166 376.82701,669.\
-1875 378.45965,668.9609 C 380.05633,668.73502 381.639,668.42\
-532 383.22608,668.14241 C 384.79034,667.88127 386.33919,667.\
-53818 387.89293,667.22433 C 389.43429,666.90277 390.96344,66\
-6.52725 392.49528,666.16451 C 394.03056,665.81731 395.5471,6\
-65.39645 397.06849,664.99483 C 398.64264,664.56434 400.22202\
-,664.15336 401.79852,663.73164 C 403.36426,663.26119 404.935\
-2,662.80866 406.50541,662.3535 C 408.05433,661.88535 409.586\
-83,661.36613 411.1234,660.86012 C 412.66775,660.35111 414.18\
-526,659.76629 415.71252,659.20982 C 417.23752,658.65947 418.\
-74032,658.0508 420.25157,657.4645 C 421.78841,656.85547 423.\
-2868,656.15704 424.79163,655.47466 C 426.29961,654.81287 427\
-.76918,654.06794 429.24814,653.34475 C 430.6928,652.6429 432\
-.0654,651.81007 433.46052,651.01822 C 409.43233,665.22265 41\
-8.13774,659.96775 423.38798,656.71194 C 424.6585,655.8887 42\
-5.88948,655.00695 427.13279,654.14384 C 428.32441,653.29299 \
-429.45698,652.3653 430.60682,651.46022 C 431.47422,650.77115\
- 432.34875,650.09101 433.22174,649.40909 L 448.5228,643.3551\
-8 C 447.63804,644.01509 446.72472,644.64308 445.86728,645.34\
-265 C 444.71845,646.26217 443.57512,647.18953 442.40365,648.\
-08019 C 441.17228,648.9734 439.95693,649.89058 438.68912,650\
-.73413 C 432.83662,654.52282 427.0541,658.31426 420.69402,66\
-1.34738 C 419.27728,662.11016 417.8829,662.91763 416.41627,6\
-63.58508 C 414.92691,664.29306 413.44902,665.02549 411.93432\
-,665.67932 C 410.41153,666.3424 408.89415,667.01896 407.3392\
-1,667.60508 C 405.82142,668.184 404.30728,668.77253 402.7802\
-3,669.32688 C 401.24293,669.87049 399.71138,670.43093 398.15\
-957,670.93266 C 396.61026,671.42265 395.06745,671.93429 393.\
-5035,672.37678 C 391.92791,672.83163 390.35562,673.29957 388\
-.77032,673.71957 C 387.19381,674.14305 385.61788,674.56863 3\
-84.04086,674.99015 C 382.50743,675.37425 380.97868,675.77697\
- 379.43716,676.12841 C 377.89624,676.47487 376.35896,676.837\
-7 374.81053,677.15004 C 373.24643,677.44935 371.68464,677.76\
-125 370.11446,678.02797 C 368.51685,678.29656 366.9236,678.5\
-9385 365.31562,678.79756 C 363.67262,679.0061 362.03203,679.\
-23485 360.38388,679.40114 C 358.70992,679.54982 357.03695,67\
-9.70997 355.35705,679.78096 C 353.653,679.85299 351.94917,67\
-9.93986 350.24276,679.91999 C 348.51003,679.89948 346.77686,\
-679.89135 345.046,679.79257 C 343.26754,679.68408 341.48595,\
-679.6006 339.71726,679.37303 C 337.94225,679.10105 336.15156\
-,678.88936 334.43306,678.33765 C 332.79598,677.8244 331.1544\
-,677.3242 329.53893,676.74424 C 327.90173,676.11886 326.2496\
-3,675.52578 324.67627,674.74596 C 323.1254,673.97225 321.562\
-17,673.22067 320.07267,672.33029 C 318.56132,671.43198 317.0\
-3713,670.55306 315.58562,669.55826 C 314.11604,668.54878 312\
-.63482,667.55487 311.20896,666.48351 C 309.80579,665.41766 3\
-08.38349,664.37439 307.0683,663.19816 C 305.78429,662.03802 \
-304.47767,660.90001 303.28216,659.64604 C 302.11242,658.3914\
-3 300.9084,657.16342 299.88424,655.78196 C 298.96626,654.474\
-88 297.98646,653.20835 297.19409,651.81588 C 296.39988,650.4\
-375 295.56515,649.0813 294.85536,647.65537 C 294.13919,646.1\
-7664 293.36012,644.72494 292.78129,643.18325 C 292.22278,641\
-.62181 291.61949,640.07435 291.16208,638.47811 C 290.68499,6\
-36.81415 290.1599,635.15907 289.86919,633.44829 C 289.57968,\
-631.69328 289.25735,629.942 289.12427,628.16477 C 288.97373,\
-626.29499 288.78192,624.42493 288.82889,622.5457 C 288.86416\
-,620.6118 288.86309,618.67496 289.1145,616.75238 C 289.35851\
-,614.80657 289.56627,612.85449 289.98378,610.93512 C 290.396\
-86,608.98011 290.79545,607.02179 291.3074,605.08916 C 291.83\
-86,603.12616 292.35898,601.16001 292.95529,599.21532 C 293.5\
-5531,597.26108 294.13954,595.30137 294.82052,593.37312 C 295\
-.49687,591.47567 296.15668,589.57191 296.87156,587.68842 C 2\
-97.60792,585.82579 298.33182,583.95803 299.09663,582.10678 C\
- 299.88215,580.28367 300.65555,578.45551 301.52849,576.67149\
- C 302.37027,574.91155 303.21063,573.15093 304.0723,571.4006\
- C 304.92867,569.65917 305.77955,567.91493 306.65382,566.182\
-38 C 307.51229,564.44638 308.37352,562.71183 309.21869,560.9\
-6929 C 310.05554,559.23843 310.89164,557.50748 311.67649,555\
-.75215 C 312.47234,554.02453 313.28387,552.30409 314.03792,5\
-50.55739 C 314.82916,548.78481 315.62848,547.01578 316.40577\
-,545.23701 C 317.2127,543.40023 318.01405,541.56117 318.8030\
-7,539.71654 C 319.59553,537.83852 320.40019,535.96609 321.14\
-196,534.06683 C 321.89793,532.14643 322.66854,530.2315 323.3\
-8361,528.29533 C 324.06665,526.36841 324.76378,524.44612 325\
-.38216,522.4969 C 326.01357,520.50014 326.65767,518.50728 32\
-7.24477,516.49664 C 327.83761,514.40974 328.44098,512.32573 \
-328.96655,510.2204 C 329.50731,508.0994 330.05662,505.98047 \
-330.53336,503.84375 C 330.99109,501.68477 331.46412,499.5286\
-4 331.86906,497.35878 C 332.25347,495.1853 332.64721,493.013\
-59 332.95502,490.8274 C 333.27834,488.65205 333.61051,486.47\
-836 333.87985,484.29519 C 334.14635,482.09722 334.42445,479.\
-90067 334.63252,477.69603 C 334.85115,475.52144 335.08185,47\
-3.34776 335.26753,471.17003 C 335.44247,468.97902 335.61987,\
-466.78814 335.7857,464.59636 C 335.9374,462.43353 336.09908,\
-460.27127 336.2325,458.10726 C 336.36861,455.95054 336.50471\
-,453.79383 336.64346,451.63726 C 336.75954,449.54451 336.871\
-8,447.45154 337.00837,445.36 C 337.14218,443.32145 337.27141\
-,441.28256 337.42819,439.24561 C 337.58804,437.21771 337.738\
-7,435.18901 337.96899,433.16744 C 338.18602,431.21577 338.39\
-223,429.26293 338.69246,427.3216 C 338.96648,425.46725 339.2\
-2994,423.61113 339.55622,421.76492 C 339.87123,420.01556 340\
-.1748,418.26393 340.54188,416.52441 C 340.89585,414.85685 34\
-1.25483,413.1906 341.62509,411.52661 C 341.96789,410.02221 3\
-42.29031,408.5126 342.68484,407.02054 C 343.05884,405.68159 \
-343.40898,404.33587 343.82991,403.01022 C 344.20476,401.8347\
-4 344.56584,400.65438 344.97285,399.48945 C 345.31926,398.47\
-686 345.63507,397.45329 346.02625,396.45637 C 346.34746,395.\
-60134 346.62749,394.73113 346.98966,393.89195 C 347.27371,39\
-3.23878 347.49217,392.5551 347.8422,391.93148 C 348.06066,39\
-1.44853 348.29399,390.96969 348.49861,390.48035 L 363.56451,\
-383.66406 z \" />\012 <path\012 style=\"fill:#000000;fill-o\
-pacity:0.75;fill-rule:nonzero;stroke:none;stroke-width:1pt;s\
-troke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\"\012 \
- id=\"path2176\"\012 d=\"M 206.26655,735.18853 C 206.83\
-428,735.13263 207.3974,735.24282 207.96264,735.26997 C 208.7\
-4945,735.38766 209.54574,735.41257 210.337,735.48383 C 211.4\
-0487,735.57658 212.4743,735.65567 213.54253,735.74595 C 214.\
-85506,735.85864 216.16829,735.96544 217.48119,736.07504 C 21\
-9.09858,736.19868 220.71821,736.28835 222.33737,736.38462 C \
-224.22785,736.49412 226.11976,736.57498 228.01139,736.66113 \
-C 230.21087,736.77385 232.40938,736.90196 234.6081,737.0278 \
-C 237.0714,737.16983 239.53507,737.30307 241.99787,737.45342\
- C 244.67148,737.62094 247.34414,737.8036 250.01759,737.9742\
-6 C 252.83872,738.14605 255.65783,738.34915 258.47629,738.55\
-974 C 261.33166,738.75949 264.18381,739.00073 267.03608,739.\
-23951 C 269.88729,739.49322 272.74001,739.72881 275.59292,73\
-9.96221 C 278.4075,740.17784 281.21951,740.42472 284.0323,74\
-0.66212 C 286.81296,740.89496 289.59172,741.14985 292.37152,\
-741.39286 C 295.06522,741.63657 297.75949,741.87322 300.4530\
-1,742.11926 C 303.13613,742.35628 305.82004,742.58296 308.50\
-255,742.82687 C 311.19656,743.07346 313.89254,743.29738 316.\
-58863,743.51984 C 319.25058,743.73995 321.91436,743.93665 32\
-4.57819,744.1323 C 327.24092,744.32619 329.90506,744.49944 3\
-32.56926,744.6716 C 335.22359,744.84166 337.8814,744.9515 34\
-0.53834,745.07164 C 343.22564,745.19118 345.91516,745.25311 \
-348.60401,745.32616 C 351.31696,745.39874 354.03014,745.4615\
-8 356.7433,745.52498 C 359.44136,745.58758 362.13954,745.644\
-81 364.83774,745.70053 C 367.57326,745.73256 370.30893,745.7\
-4706 373.04456,745.76403 C 375.74646,745.78149 378.4484,745.\
-79575 381.15034,745.81059 C 383.82002,745.82382 386.48979,74\
-5.79548 389.15942,745.77758 C 391.8574,745.75953 394.55539,7\
-45.74216 397.25338,745.7246 C 399.91742,745.69135 402.58082,\
-745.63331 405.24395,745.55765 C 407.87129,745.50812 410.4949\
-8,745.36564 413.11893,745.23177 C 415.73845,745.09297 418.35\
-327,744.88722 420.96717,744.66993 C 423.51009,744.47596 426.\
-03684,744.14066 428.56376,743.80513 C 431.09356,743.45034 43\
-3.61486,743.042 436.1332,742.61487 C 438.63924,742.2061 441.\
-1236,741.68002 443.60982,741.16769 C 446.05806,740.633 448.5\
-0068,740.07365 450.9482,739.5357 C 453.40114,739.00595 455.8\
-4213,738.42516 458.28253,737.84124 C 460.69422,737.27756 463\
-.08874,736.64498 465.48757,736.02949 C 467.86795,735.42847 4\
-70.23683,734.78314 472.6091,734.1515 C 474.96773,733.51309 4\
-77.3144,732.83138 479.66421,732.16211 C 482.02051,731.48263 \
-484.35755,730.73855 486.69943,730.01148 C 488.98277,729.2684\
-1 491.25255,728.48635 493.5211,727.69967 C 495.74694,726.936\
-98 497.95792,726.13263 500.17079,725.33341 C 502.40879,724.5\
-2209 504.6282,723.66124 506.85109,722.80971 C 509.07688,721.\
-93485 511.30134,721.05647 513.52615,720.17909 C 515.67658,71\
-9.35468 517.79651,718.45525 519.92,717.56489 C 521.99762,716\
-.70152 524.05115,715.78147 526.11272,714.88069 C 528.11898,7\
-14.02833 530.1048,713.13028 532.09212,712.2351 C 534.05859,7\
-11.36195 536.02997,710.50006 538.00085,709.63697 C 539.90815\
-,708.77989 541.81101,707.91315 543.71275,707.04382 C 545.595\
-86,706.14929 547.48078,705.25855 549.36482,704.36601 C 550.2\
-541,703.91604 551.18227,703.55198 552.09107,703.14493 L 539.\
-24675,713.31982 C 538.34968,713.73375 537.44287,714.12701 53\
-6.55531,714.56169 C 534.67153,715.45623 532.79173,716.35952 \
-530.89422,717.22476 C 528.98861,718.0878 527.08548,718.95654\
- 525.16954,719.79655 C 523.20122,720.65889 521.23139,721.517\
-91 519.27311,722.40296 C 517.28095,723.28623 515.28912,724.1\
-7001 513.28789,725.03273 C 511.22562,725.93162 509.1655,726.\
-83555 507.08618,727.6946 C 504.95435,728.56704 502.82537,729\
-.44643 500.67673,730.27721 C 498.45095,731.151 496.22608,732\
-.02716 493.99544,732.8885 C 491.76535,733.73663 489.53765,73\
-4.59119 487.29042,735.39333 C 485.07262,736.18412 482.85606,\
-736.9783 480.62694,737.73693 C 478.35074,738.51017 476.07912\
-,739.2981 473.7808,740.00435 C 471.43416,740.72754 469.09091\
-,741.46253 466.72735,742.12936 C 464.37161,742.79347 462.017\
-78,743.46469 459.65348,744.09791 C 457.27768,744.72254 454.9\
-0331,745.3528 452.52239,745.95783 C 450.11973,746.56848 447.\
-71825,747.18361 445.30364,747.74595 C 442.85614,748.31887 44\
-0.40672,748.88318 437.95004,749.41578 C 435.50275,749.96341 \
-433.05776,750.52223 430.60157,751.02901 C 428.10469,751.5294\
-5 425.60682,752.02583 423.09172,752.42808 C 420.56033,752.83\
-41 418.02848,753.23913 415.4862,753.57247 C 412.94418,753.88\
-674 410.40025,754.18248 407.84474,754.36767 C 405.22137,754.\
-56722 402.5975,754.76255 399.96919,754.88716 C 397.33809,755\
-.01363 394.70672,755.12897 392.07288,755.18189 C 389.40449,7\
-55.24329 386.7359,755.29941 384.06678,755.31801 C 381.36875,\
-755.33501 378.67073,755.35242 375.97272,755.37191 C 373.2990\
-3,755.38931 370.62523,755.41374 367.95151,755.38874 C 365.25\
-018,755.37285 362.54886,755.35643 359.84754,755.339 C 357.10\
-766,755.31917 354.36761,755.30541 351.62803,755.2566 C 348.9\
-2819,755.2002 346.22832,755.14489 343.52869,755.07854 C 340.\
-81464,755.01316 338.1006,754.94719 335.38672,754.87448 C 332\
-.69076,754.79861 329.99438,754.7288 327.30034,754.59865 C 32\
-4.63702,754.47443 321.97325,754.355 319.3131,754.17258 C 316\
-.64644,753.99677 313.97981,753.81979 311.31469,753.62143 C 3\
-08.64815,753.42194 305.98167,753.22127 303.31717,752.99564 C\
- 300.61972,752.76816 297.92242,752.539 295.22668,752.29197 C\
- 292.54232,752.05594 289.85729,751.82776 287.17355,751.58489\
- C 284.48325,751.34013 281.79236,751.10214 279.10203,750.857\
-82 C 276.32458,750.61156 273.54785,750.3567 270.76879,750.12\
-897 C 267.95809,749.88986 265.14741,749.65067 262.3352,749.4\
-2984 C 259.48229,749.192 256.62979,748.94948 253.77775,748.7\
-0157 C 250.92829,748.46986 248.07881,748.23864 245.22702,748\
-.03704 C 242.40911,747.83606 239.59086,747.63932 236.77071,7\
-47.47191 C 234.09337,747.30158 231.41656,747.12239 228.73827\
-,746.96768 C 226.269,746.82962 223.79932,746.69923 221.33017\
-,746.55878 C 219.12742,746.44441 216.92494,746.32277 214.721\
-13,746.22959 C 212.81711,746.14391 210.91276,746.06422 209.0\
-1003,745.95189 C 207.37594,745.85328 205.74134,745.76175 204\
-.10919,745.63339 C 202.78951,745.52596 201.47005,745.4154 20\
-0.14929,745.32195 C 199.07382,745.24115 197.99943,745.14624 \
-196.92282,745.0813 C 196.13428,745.02941 195.34766,744.93008\
- 194.55682,744.92561 C 194.04688,744.97798 193.52122,744.934\
-4 193.02027,745.08341 L 206.26655,735.18853 z \" />\012 </g>\012</\
-svg>\012"
+data = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\012<svg\012\
+ xmlns=\"http://www.w3.org/2000/svg\"\012 xmlns:xlink=\"http:/\
+/www.w3.org/1999/xlink\"\012 width=\"737\"\012 height=\"923\"\012 ve\
+rsion=\"1.0\">\012 <defs>\012 <linearGradient\012 id=\"linearG\
+radient3152\">\012 <stop\012 style=\"stop-color:#b8ffb4\
+;stop-opacity:1;\"\012 offset=\"0\" />\012 <stop\012 \
+ offset=\"0.5\"\012 style=\"stop-color:#2eff22;stop-opaci\
+ty:0.5;\" />\012 <stop\012 style=\"stop-color:#ffffff;s\
+top-opacity:0;\"\012 offset=\"1\" />\012 </linearGradient>\
+\012 <radialGradient\012 xlink:href=\"#linearGradient3152\"\
+\012 id=\"radialGradient3158\"\012 cx=\"260\"\012 cy=\"2\
+35\"\012 fx=\"260\"\012 fy=\"235\"\012 r=\"259\"\012 gr\
+adientTransform=\"matrix(1,0,0,1.2531846,0,-59.560934)\"\012 \
+ gradientUnits=\"userSpaceOnUse\" />\012 </defs>\012 <g\012 tran\
+sform=\"translate(-3,-73)\">\012 <path\012 style=\"opacity:1\
+;color:#000000;fill:url(#radialGradient3158);fill-opacity:1;\
+fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-lineca\
+p:butt;stroke-linejoin:miter;marker:none;marker-start:none;m\
+arker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-da\
+sharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility\
+:visible;display:inline;overflow:visible\"\012 id=\"path217\
+8\"\012 d=\"M 519 235 A 259 324 0 1 1 0,235 A 259 324 0 1 \
+1 519 235 z\"\012 transform=\"matrix(1.4203822,0,0,1.42038\
+22,0,200)\" />\012 <path\012 style=\"fill:#000000;fill-opac\
+ity:0.75;fill-rule:nonzero;stroke:none;stroke-width:1pt;stro\
+ke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\"\012 \
+ d=\"M 420,366 C 438,381 455,400 478,408 C 523,427 576,424 \
+620,405 C 632,400 644,393 655,387 C 652,389 638,397 649,391 \
+C 658,385 666,379 676,376 C 688,370 673,379 669,382 C 637,40\
+1 604,421 566,427 C 526,435 482,429 446,408 C 431,398 419,38\
+5 405,374 C 410,371 415,368 420,366 z \" />\012 <path\012 \
+style=\"fill:#000000;fill-opacity:0.75;fill-rule:nonzero;stro\
+ke:none;stroke-width:1pt;stroke-linecap:butt;stroke-linejoin\
+:miter;stroke-opacity:1\"\012 d=\"M 322,366 C 303,381 286,4\
+00 263,408 C 218,427 166,424 121,405 C 109,400 98,393 86,387\
+ C 89,389 103,397 93,391 C 84,385 75,379 65,376 C 53,370 68,\
+379 72,382 C 104,401 137,421 175,427 C 216,435 260,429 295,4\
+08 C 310,398 322,385 336,374 C 331,371 326,368 322,366 z \" /\
+>\012 <path\012 style=\"fill:#000000;fill-opacity:0.75;fil\
+l-rule:nonzero;stroke:none;stroke-width:1pt;stroke-linecap:b\
+utt;stroke-linejoin:miter;stroke-opacity:1\"\012 d=\"M 363,\
+383 C 347,418 353,458 345,495 C 339,525 324,551 312,579 C 30\
+4,598 298,620 309,639 C 317,655 335,667 353,669 C 379,671 40\
+5,664 429,653 C 442,646 405,667 423,656 C 429,652 434,647 44\
+1,645 C 455,639 439,650 434,653 C 408,669 378,679 347,679 C \
+327,679 308,667 297,651 C 285,634 287,613 294,594 C 302,570 \
+316,548 324,523 C 335,493 335,460 338,428 C 340,415 342,401 \
+349,390 C 353,388 358,385 363,383 z \" />\012 <path\012 st\
+yle=\"fill:#000000;fill-opacity:0.75;fill-rule:nonzero;stroke\
+:none;stroke-width:1pt;stroke-linecap:butt;stroke-linejoin:m\
+iter;stroke-opacity:1\"\012 d=\"M 206,735 C 245,737 285,740\
+ 324,744 C 357,745 391,746 424,744 C 468,738 510,723 550,703\
+ C 552,703 544,709 541,711 C 531,718 518,722 507,727 C 474,7\
+40 440,751 405,754 C 360,756 314,754 268,749 C 243,747 218,7\
+46 193,745 C 197,741 201,738 206,735 z \" />\012 </g>\012</svg>\012"
### end
diff --git a/olpcgames/dbusproxy.py b/olpcgames/dbusproxy.py
new file mode 100644
index 0000000..a103e28
--- /dev/null
+++ b/olpcgames/dbusproxy.py
@@ -0,0 +1,93 @@
+"""Spike test for a safer networking system for DBUS-based objects"""
+from olpcgames import eventwrap, util
+from dbus import proxies
+import logging
+log = logging.getLogger( 'dbus' )
+log.setLevel( logging.DEBUG )
+
+def wrap( value, tube=None,path=None ):
+ """Wrap object with any required pygame-side proxies"""
+ if isinstance( value,proxies._ProxyMethod ):
+ return DBUSMethod( value, tube=tube, path=path )
+ elif isinstance( value, proxies._DeferredMethod ):
+ value._proxy_method = DBUSMethod( value._proxy_method, tube=tube, path=path )
+ return value
+ elif isinstance( value, proxies.ProxyObject ):
+ return DBUSProxy( value, tube=tube, path=path )
+ else:
+ return value
+
+class DBUSProxy( object ):
+ """Proxy for the DBUS Proxy object"""
+ def __init__( self, proxy, tube=None, path=None ):
+ log.info( 'Creating Pygame-side proxy for %s (%s)', proxy,path )
+ self.__proxy = proxy
+ self.__tube = tube
+ self.__path = path
+ def __getattr__( self, key ):
+ """Retrieve attribute of given key"""
+ from dbus import proxies
+ return wrap( getattr( self.__proxy, key ) )
+ def add_signal_receiver( self, callback, eventName, interface, path=None, sender_keyword='sender'):
+ """Add a new signal handler (which will be called many times) for given signal
+ """
+ log.info( """Setting signal receiver %s for event %s on interface %s (object path %s) with sender_keyword = %r""",
+ callback, eventName, interface, path, sender_keyword,
+ )
+ log.debug( """proxy: %s proxy.tube: %s""", self.__proxy, self.__proxy.tube )
+ self.__tube.add_signal_receiver(
+ Callback( callback ),
+ eventName,
+ interface,
+ path = path or self.__path,
+ sender_keyword = sender_keyword,
+ )
+
+class DBUSMethod( object ):
+ """DBUS method which does callbacks in the Pygame (eventwrapper) thread"""
+ def __init__( self, proxy, tube,path ):
+ log.info( 'Creating Pygame-side method proxy for %s', proxy )
+ self.__proxy = proxy
+ self.__tube = tube
+ self.__path = path
+ def __call__( self, *args, **named ):
+ """Perform the asynchronous call"""
+ log.info( 'Calling proxy for %s with *%s, **%s', self.__proxy, args, named )
+ callback, errback = named.get( 'reply_handler'), named.get( 'error_handler' )
+ if not callback:
+ raise TypeError( """Require a reply_handler named argument to do any asynchronous call""" )
+ else:
+ callback = Callback( callback )
+ if not errback:
+ errback = defaultErrback
+ else:
+ errback = Callback( errback )
+ named['reply_handler'] = callback
+ named['error_handler'] = errback
+ return self.__proxy( *args, **named )
+
+def defaultErrback( error ):
+ """Log the error to stderr/log"""
+ log.error( """Failure in DBUS call: %s""", error )
+
+class Callback( object ):
+ """PyGTK-side callback which generates a CallbackResult to process on the Pygame side"""
+ def __init__( self, callable, callContext = None):
+ """Initialize the callback to process results"""
+ self.callable = callable
+ if callContext is None:
+ callContext = util.get_traceback( None )
+ self.callContext = callContext
+ def __call__( self, *args, **named ):
+ """PyGTK-side callback operation"""
+ log.info( 'Callback %s return value *%s, **%s', self.callable, args, named )
+ from olpcgames import eventwrap
+ args = [wrap(a) for a in args]
+ named = dict([
+ (k,wrap(v)) for k,v in named.items()
+ ])
+ eventwrap.post(
+ eventwrap.CallbackResult(
+ self.callable, args, named, callContext = self.callContext
+ )
+ )
diff --git a/olpcgames/eventwrap.py b/olpcgames/eventwrap.py
index 92fbabb..402109c 100644
--- a/olpcgames/eventwrap.py
+++ b/olpcgames/eventwrap.py
@@ -4,32 +4,131 @@ Provides methods which will be substituted into Pygame in order to
provide the synthetic events that we will feed into the Pygame queue.
These methods are registered by the "install" method.
-Extension:
+This event queue does not support getting events only of a certain type.
+You need to get all pending events at a time, or filter them yourself. You
+can, however, block and unblock events of certain types, so that may be
+useful to you.
- last_event_time() -- returns period since the last event was produced
- in seconds. This can be used to create "pausing" effects for games.
+Set_grab doesn't do anything (you are not allowed to grab events). Sorry.
+
+Extensions:
+
+ wait( timeout=None ) -- allows you to wait for only a specified period
+ before you return to the application. Can be used to e.g. wait for a
+ short period, then release some resources, then wait a bit more, then
+ release a few more resources, then a bit more...
"""
import pygame
import gtk
import Queue
-import thread
+import thread, threading
import logging
+from olpcgames import util
log = logging.getLogger( 'olpcgames.eventwrap' )
-# This module reuses Pygame's Event, but
-# reimplements the event queue.
from pygame.event import Event, event_name, pump as pygame_pump, get as pygame_get
class Event(object):
"""Mock pygame events"""
- def __init__(self, type, **named):
+ def __init__(self, type, dict=None,**named):
+ """Initialise the new event variables from dictionary and named become attributes"""
self.type = type
+ if dict:
+ self.__dict__.update( dict )
self.__dict__.update( named )
+ def _get_dict( self ):
+ return self.__dict__
+ dict = property( _get_dict )
+ def __repr__( self ):
+ result = []
+ for key,value in self.__dict__.items():
+ if not key.startswith( '_' ):
+ result.append( '%s = %r'%( key, value ))
+ return '%s( %s, %s )'%(
+ self.__class__.__name__,
+ self.type,
+ ",".join( result ),
+ )
+ def block( self ):
+ """Block until this event is finished processing
+
+ Event process is only finalized on the *next* call to retrieve an event
+ after the processing operation in which the event is processed. In some
+ extremely rare cases we might actually see that happen, were the
+ file-saving event (for example) causes the Pygame event loop to exit.
+ In that case, the GTK event loop *could* hang.
+ """
+ log.info( '''Blocking GTK thread on event: %s''', self )
+ self.__lock = threading.Event()
+ self.__lock.wait()
+ def retire( self ):
+ """Block the GTK event loop until this event is processed"""
+ try:
+ self.__lock.set()
+ log.info( '''Released GTK thread on event: %s''', self )
+ except AttributeError, err:
+ pass
+
+class CallbackResult( object ):
+ def __init__( self, callable, args, named, callContext=None ):
+ """Perform callback in Pygame loop with args and named
+
+ callContext is used to provide more information when there is
+ a failure in the callback (for debugging purposes)
+ """
+ self.callable = callable
+ self.args = args
+ self.named = named
+ if callContext is None:
+ callContext = util.get_traceback( None )
+ self.callContext = callContext
+ def __call__( self ):
+ """Perform the actual callback in the Pygame event loop"""
+ try:
+ self.callable( *self.args, **self.named )
+ except Exception, err:
+ log.error(
+ """Failure in callback %s( *%s, **%s ): %s\n%s""",
+ getattr(self.callable, '__name__',self.callable),
+ self.args, self.named,
+ util.get_traceback( err ),
+ self.callContext
+ )
+
-#print "Initializing own event.py"
+_EVENTS_TO_RETIRE = []
+
+def _releaseEvents( ):
+ """Release/retire previously-processed events"""
+ if _EVENTS_TO_RETIRE:
+ for event in _EVENTS_TO_RETIRE:
+ try:
+ event.retire()
+ except AttributeError, err:
+ pass
+
+def _processCallbacks( events ):
+ """Process any callbacks in events and remove from the stream"""
+ result = []
+ for event in events:
+ if isinstance( event, CallbackResult ):
+ event()
+ else:
+ result.append( event )
+ if events and not result:
+ result.append(
+ Event( type=pygame.NOEVENT )
+ )
+ return result
+
+def _recordEvents( events ):
+ """Record the set of events to retire on the next iteration"""
+ global _EVENTS_TO_RETIRE
+ events = _processCallbacks( events )
+ _EVENTS_TO_RETIRE = events
+ return events
-# Install myself on top of pygame.event
def install():
"""Installs this module (eventwrap) as an in-place replacement for the pygame.event module.
@@ -41,31 +140,122 @@ def install():
registers the handler itself, so you will always wind up with it registered when
you use olpcgames (the gtkEvent.Translator.hook_pygame method calls it).
"""
- import eventwrap,pygame
+ log.info( 'Installing OLPCGames event wrapper' )
+ from olpcgames import eventwrap
+ import pygame
pygame.event = eventwrap
import sys
sys.modules["pygame.event"] = eventwrap
-
# Event queue:
-g_events = Queue.Queue()
+class _FilterQueue( Queue.Queue ):
+ """Simple Queue sub-class with a put_left method"""
+ def get_type( self, filterFunction, block=True, timeout=None ):
+ """Get events of a given type
+
+ Note: can raise Empty *even* when blocking if someone else
+ pops the event off the queue before we get around to it.
+ """
+ self.not_empty.acquire()
+ try:
+ if not block:
+ if self._empty_type( filterFunction ):
+ raise Queue.Empty
+ elif timeout is None:
+ while self._empty_type( filterFunction ):
+ self.not_empty.wait()
+ else:
+ if timeout < 0:
+ raise ValueError("'timeout' must be a positive number")
+ endtime = _time() + timeout
+ while self._empty_type( filterFunction ):
+ remaining = endtime - _time()
+ if remaining <= 0.0:
+ raise Queue.Empty
+ self.not_empty.wait(remaining)
+ item = self._get_type( filterFunction )
+ self.not_full.notify()
+ return item
+ finally:
+ self.not_empty.release()
+ def _empty_type( self, filterFunction ):
+ """Are we empty with respect to filterFunction?"""
+ for element in self.queue:
+ if filterFunction( element ):
+ return False
+ return True
+ def _get_type( self, filterFunction ):
+ """Get the first instance which matches filterFunction"""
+ for element in self.queue:
+ if filterFunction( element ):
+ self.queue.remove( element )
+ return element
+ # someone popped the event off the queue before we got to it!
+ raise Queue.Empty
+ def peek_type( self, filterFunction= lambda x: True ):
+ """Peek to see if we have filterFunction-matching element
+
+ Note: obviously this is *not* thread safe, it's just informative...
+ """
+ try:
+ for element in self.queue:
+ if filterFunction( element ):
+ return element
+ return None
+ except RuntimeError, err:
+ return None # none yet, at least
+
+g_events = _FilterQueue()
# Set of blocked events as set by set
g_blocked = set()
-g_blockedlock = thread.allocate_lock()
+g_blockedlock = thread.allocate_lock() # should use threading instead
g_blockAll = False
+def _typeChecker( types ):
+ """Create check whether an event is in types"""
+ try:
+ if 1 in types:
+ pass
+ def check( element ):
+ return element.type in types
+ return check
+ except TypeError, err:
+ def check( element ):
+ return element.type == types
+ return check
+
def pump():
- """Handle any window manager and other external events that aren't passed to the user. Call this periodically (once a frame) if you don't call get(), poll() or wait()."""
+ """Handle any window manager and other external events that aren't passed to the user
+
+ Call this periodically (once a frame) if you don't call get(), poll() or wait()
+ """
pygame_pump()
+ _releaseEvents()
-def get():
- """Get a list of all pending events. (Unlike pygame, there's no option to filter by event type; you should use set_blocked() if you don't want to see certain events.)"""
+def get( types=None):
+ """Get a list of all pending events
+
+ types -- either an integer event-type or a sequence of integer event types
+ which restrict the set of event-types returned from the queue. Keep in mind
+ that if you do not remove events you may wind up with an eternally growing
+ queue or a full queue. Normally you will want to remove all events in your
+ top-level event-loop and propagate them yourself.
+
+ Note: if you use types you lose all event ordering guarantees, events
+ may show up after events which were originally produced before them due to
+ the re-ordering of the queue on filtering!
+ """
pump()
eventlist = []
try:
- while True:
- eventlist.append(g_events.get(block=False))
+ if types:
+ check = _typeChecker( types )
+ while True:
+ eventlist.append(g_events.get_type( check, block=False))
+ else:
+ while True:
+ eventlist.append(g_events.get(block=False))
except Queue.Empty:
pass
@@ -73,32 +263,14 @@ def get():
if pygameEvents:
log.info( 'Raw Pygame events: %s', pygameEvents)
eventlist.extend( pygameEvents )
- if eventlist:
- _set_last_event_time()
- return eventlist
-
-_LAST_EVENT_TIME = 0
-
-def _set_last_event_time( time=None ):
- """Set this as the last event time"""
- global _LAST_EVENT_TIME
- if time is None:
- time = pygame.time.get_ticks()
- _LAST_EVENT_TIME = time
- return time
-
-def last_event_time( ):
- """Return the last event type for pausing operations in seconds"""
- global _LAST_EVENT_TIME
- return (pygame.time.get_ticks() - _LAST_EVENT_TIME)/1000.
+ return _recordEvents( eventlist )
def poll():
"""Get the next pending event if exists. Otherwise, return pygame.NOEVENT."""
pump()
try:
result = g_events.get(block=False)
- _set_last_event_time()
- return result
+ return _recordEvents( [result] )[0]
except Queue.Empty:
return Event(pygame.NOEVENT)
@@ -107,30 +279,49 @@ def wait( timeout = None):
"""Get the next pending event, wait up to timeout if none
timeout -- if present, only wait up to timeout seconds, if we
- do not find an event before then, return None
+ do not find an event before then, return None. timeout
+ is an OLPCGames-specific extension.
"""
pump()
try:
+ result = None
result = g_events.get(block=True, timeout=timeout)
- _set_last_event_time()
- return result
+ try:
+ return _recordEvents( [result] )[0]
+ except IndexError, err:
+ return Event( type=pygame.NOEVENT )
except Queue.Empty, err:
return None
def peek(types=None):
- """True if there is any pending event. (Unlike pygame, there's no option to
- filter by event type)"""
+ """True if there is any pending event
+
+ types -- optional set of event-types used to check whether
+ an event is of interest. If specified must be either a sequence
+ of integers/longs or an integer/long.
+ """
+ if types:
+ check = _typeChecker( types )
+ return g_events.peek_type( check ) is not None
return not g_events.empty()
def clear():
- """Dunno why you would do this, but throws every event out of the queue"""
+ """Clears the entire pending queue of events
+
+ Rarely used
+ """
try:
+ discarded = []
while True:
- g_events.get(block=False)
+ discarded.append( g_events.get(block=False) )
+ discarded = _recordEvents( discarded )
+ _releaseEvents()
+ return discarded
except Queue.Empty:
pass
def set_blocked(item):
+ """Block item/items from being added to the event queue"""
g_blockedlock.acquire()
try:
# FIXME: we do not currently know how to block all event types when
@@ -140,6 +331,7 @@ def set_blocked(item):
g_blockedlock.release()
def set_allowed(item):
+ """Allow item/items to be added to the event queue"""
g_blockedlock.acquire()
try:
if item is None:
@@ -160,18 +352,16 @@ def get_blocked(*args, **kwargs):
g_blockedlock.release()
def set_grab(grabbing):
- # We don't do this.
- pass
+ """This method will not be implemented"""
def get_grab():
- # We don't do this.
- return False
+ """This method will not be implemented"""
def post(event):
- #print "posting on own"
+ """Post a new event to the Queue of events"""
g_blockedlock.acquire()
try:
- if event.type not in g_blocked:
+ if getattr(event,'type',None) not in g_blocked:
g_events.put(event, block=False)
finally:
g_blockedlock.release()
diff --git a/olpcgames/gtkEvent.py b/olpcgames/gtkEvent.py
index ce4f9eb..6b20102 100644
--- a/olpcgames/gtkEvent.py
+++ b/olpcgames/gtkEvent.py
@@ -7,7 +7,7 @@ import pygame
from olpcgames import eventwrap
import logging
log = logging.getLogger( 'olpcgames.gtkevent' )
-#log.setLevel( logging.DEBUG )
+##log.setLevel( logging.DEBUG )
class _MockEvent(object):
"""Used to inject key-repeat events on the gtk side."""
@@ -98,13 +98,29 @@ class Translator(object):
self.__tick_id = None
#print "translator initialized"
- mainwindow.connect( 'expose-event', self.do_expose_event )
+ self._inner_evb.connect( 'expose-event', self.do_expose_event )
+# screen = gtk.gdk.screen_get_default()
+# screen.connect( 'size-changed', self.do_resize_event )
+ self._inner_evb.connect( 'configure-event', self.do_resize_event )
def do_expose_event(self, event, widget):
"""Handle exposure event (trigger redraw by gst)"""
log.info( 'Expose event: %s', event )
from olpcgames import eventwrap
eventwrap.post( eventwrap.Event( eventwrap.pygame.VIDEOEXPOSE ))
return True
+ def do_resize_event( self, activity, event ):
+ """Our screen (actually, the default screen) has resized"""
+ log.info( 'Resize event: %s %s', activity, event )
+ log.info( 'Event values: %s', (event.width,event.height) )
+# from olpcgames import eventwrap
+# # shouldn't the activity's window have this information too?
+# eventwrap.post(
+# eventwrap.Event(
+# eventwrap.pygame.VIDEORESIZE,
+# dict(size=(event.width,event.height), width=event.width, height=event.height)
+# )
+# )
+ return False # continue processing
def hook_pygame(self):
"""Hook the various Pygame features so that we implement the event APIs"""
# Pygame should be initialized. Hijack their key and mouse methods
@@ -164,6 +180,9 @@ class Translator(object):
keycode = getattr(pygame, 'K_'+key.upper())
elif hasattr(pygame, 'K_'+key.lower()):
keycode = getattr(pygame, 'K_'+key.lower())
+ elif key == 'XF86Start':
+ # view source request, specially handled...
+ self._mainwindow.view_source()
else:
print 'Key %s unrecognized'%key
diff --git a/olpcgames/mesh.py b/olpcgames/mesh.py
index fb583bb..1ad4c43 100644
--- a/olpcgames/mesh.py
+++ b/olpcgames/mesh.py
@@ -1,7 +1,125 @@
-'''mesh.py: utilities for wrapping the mesh and making it accessible to Pygame'''
+'''Utilities for wrapping the telepathy network for Pygame
+
+The 'mesh' module allows your Pygame game to be Shared
+across the OLPC networking infrastructure (D-bus and Tubes).
+It offers a simplified view of the Telepathy system.
+
+All Sugar activities have a 'Share' menu (toolbar) which is
+intended to allow other people to join the activity instance
+and collaborate with you. When you select Share, the activity's
+icon appears on the Neighborhood view of other laptops.
+
+If you do nothing else with networking, this is all that will
+happen: if anyone selects your shared activity icon, they will
+just spawn a new instance of the activity, and they will get to
+play your game alone.
+
+The mesh module automatically sets up a connection from each
+participant to every other participant. It provides (string based)
+communications channels that let you either broadcast messages
+to other users or communicate point-to-point to one other user.
+
+You can use the "handles" which uniquely idenify users to send
+messages to an individual user (send_to( handle, message )) or
+broadcast( message ) to send a message to all participants.
+
+More advanced (structured) networking can be handled by using
+the get_object( handle, path ) function, which looks up an object
+(by DBUS path) shared by the user "handle" and returns a
+DBUS/Telepathy proxy for that object. The object you get back is
+actually an olpcgames.dbusproxy.DBUSProxy instance, which
+enforces asynchronous operations and runs your
+reply_handler/error_handler in the Pygame event loop.
+
+NOTE:
+ You *cannot* make synchronous calls on these objects!
+ You must use the named arguments:
+
+ reply_handler, error_handler
+
+ for every call which you perform on a shared object (normally
+ these are ExportedGObject instances).
+
+If you want to run your callbacks in the GTK event loop (for instance
+because they need to handle GTK-side objects), you can use the
+dbus_get_object function. This is *not* recommended for normal
+usage, as any call to Pygame operations within the GTK event loop
+can cause a segfault/core of your entire Activity.
+
+Note:
+
+ mesh sets up N**2 connections for each shared activity, obviously
+ that will not scale to very large shared activities.
+
+Note:
+
+ The intention is that mesh will be refactored, possibly as a
+ new module called "olpcgames.network", which would break out
+ the various components so that there is no longer an assumed
+ networking layout. We will attempt to retain the mesh module's
+ API as we do so.
+
+Events produced:
+
+ olpcgames.CONNECT -- The tube connection was started. (i.e., the
+ user clicked Share or started the activity from the Neighborhood
+ screen).
+
+ Event properties:
+
+ id -- a unique identifier for this connection. (shouldn't be needed
+ for anything)
+
+ olpcgames.PARTICIPANT_ADD -- A participant joined the activity.
+ This will trigger for the local user as well as any arriving remote
+ users. Note that this *only* occurs after the activity is shared,
+ that is, the local user does not appear until after they have
+ shared a locally-started activity.
+
+ Event properties:
+
+ handle -- the arriving user's handle (a uniquely identifying string
+ assigned to the user by the Telepathy system, not human
+ readable), see lookup_buddy to retrieve human-readable
+ descriptions of the user.
+
+ olpcgames.PARTICIPANT_REMOVE -- A participant quit the activity.
+
+ Event properties:
+
+ handle -- the departing user's handle.
+
+ olpcgames.MESSAGE_UNI -- A message was sent to you.
+
+ Event properties:
+
+ content -- the content of the message (a string)
+ handle -- the handle of the sending user.
+
+ olpcgames.MESSAGE_MULTI -- A message was sent to everyone.
+
+ Event properties:
+
+ content -- the content of the message (a string)
+ handle -- the handle of the sending user.
+
+Note:
+
+ Eventually we will stop using top-level Pygame event types for the
+ various networking message types (currently four of them). We will
+ likely use UserEvent with a sub-type specifier for the various events
+ that OLPCGames produces.
+
+See Also:
+
+ http://blog.vrplumber.com/2016 -- Discussion of how Productive uses
+ the mesh module and raw Telepathy (ExportedGObject instances)
+'''
import logging
-log = logging.getLogger('olpcgames.mesh')
-#log.setLevel( logging.DEBUG )
+log = logging.getLogger( 'olpcgames.mesh' )
+##log.setLevel( logging.DEBUG )
+import olpcgames
+from olpcgames.util import get_traceback
try:
from sugar.presence.tubeconn import TubeConnection
except ImportError, err:
@@ -17,45 +135,27 @@ try:
except ImportError, err:
telepathy = None
+try:
+ import sugar.presence.presenceservice
+except Exception, err:
+ pass
+import pygame.event as PEvent
-class OfflineError(Exception):
+class OfflineError( Exception ):
"""Raised when we cannot complete an operation due to being offline"""
-DBUS_IFACE = "org.laptop.games.pygame"
-DBUS_PATH = "/org/laptop/games/pygame"
+DBUS_IFACE="org.laptop.games.pygame"
+DBUS_PATH="/org/laptop/games/pygame"
DBUS_SERVICE = None
### NEW PYGAME EVENTS ###
-'''The tube connection was started. (i.e., the user clicked Share or started
-the activity from the Neighborhood screen).
-Event properties:
- id: a unique identifier for this connection. (shouldn't be needed for anything)'''
-CONNECT = 9912
-
-'''A participant joined the activity. This will trigger for the local user
-as well as any arriving remote users.
-Event properties:
- handle: the arriving user's handle.'''
-PARTICIPANT_ADD = 9913
-
-'''A participant quit the activity.
-Event properties:
- handle: the departing user's handle.'''
-PARTICIPANT_REMOVE = 9914
-
-'''A message was sent to you.
-Event properties:
- content: the content of the message (a string)
- handle: the handle of the sending user.'''
-MESSAGE_UNI = 9915
-
-'''A message was sent to everyone.
-Event properties:
- content: the content of the message (a string)
- handle: the handle of the sending user.'''
-MESSAGE_MULTI = 9916
+CONNECT = olpcgames.CONNECT
+PARTICIPANT_ADD = olpcgames.PARTICIPANT_ADD
+PARTICIPANT_REMOVE = olpcgames.PARTICIPANT_REMOVE
+MESSAGE_UNI = olpcgames.MESSAGE_UNI
+MESSAGE_MULTI = olpcgames.MESSAGE_MULTI
# Private objects for useful purposes!
@@ -67,21 +167,18 @@ joining = False
connect_callback = None
-
def is_initiating():
'''A version of is_initiator that's a bit less goofy, and can be used
before the Tube comes up.'''
global initiating
return initiating
-
def is_joining():
'''Returns True if the activity was started up by means of the
Neighbourhood mesh view.'''
global joining
return joining
-
def set_connect_callback(cb):
'''Just the same as the Pygame event loop can listen for CONNECT,
this is just an ugly callback that the glib side can use to be aware
@@ -89,7 +186,6 @@ def set_connect_callback(cb):
global connect_callback
connect_callback = cb
-
def activity_shared(activity):
'''Called when the user clicks Share.'''
@@ -98,9 +194,10 @@ def activity_shared(activity):
_setup(activity)
+
log.debug('This is my activity: making a tube...')
channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
- if hasattr(channel, 'OfferDBusTube'):
+ if hasattr( channel, 'OfferDBusTube' ):
id = channel.OfferDBusTube(
DBUS_SERVICE, {})
else:
@@ -111,7 +208,6 @@ def activity_shared(activity):
if connect_callback is not None:
connect_callback()
-
def activity_joined(activity):
'''Called at the startup of our Activity, when the user started it via Neighborhood intending to join an existing activity.'''
@@ -121,11 +217,13 @@ def activity_joined(activity):
for buddy in activity._shared_activity.get_joined_buddies():
log.debug('Buddy %s is already in the activity' % buddy.props.nick)
+
global initiating
global joining
initiating = False
joining = True
+
_setup(activity)
tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
@@ -136,14 +234,24 @@ def activity_joined(activity):
if connect_callback is not None:
connect_callback()
-
-def _getConn():
- log.info('_getConn')
- pservice = _get_presence_service()
- name, path = pservice.get_preferred_connection()
+def _getConn( activity ):
+ log.debug( '_getConn' )
global conn
- conn = telepathy.client.Connection(name, path)
- return conn
+ if conn:
+ return conn
+ else:
+ if hasattr( activity._shared_activity, 'telepathy_conn' ):
+ log.debug( '''new-style api for retrieving telepathy connection present''' )
+ conn = activity._shared_activity.telepathy_conn
+ else:
+ pservice = _get_presence_service()
+ log.debug( '_get_presence_service -> %s', pservice )
+ name, path = pservice.get_preferred_connection()
+ log.debug( '_get_presence_service -> %s, %s', name, path)
+ conn = telepathy.client.Connection(name, path)
+ log.debug( 'Telepathy Client Connection: %s', conn )
+ return conn
+
def _setup(activity):
@@ -152,37 +260,48 @@ channel present, creates one. Updates text_chan and tubes_chan.
setup(sugar.activity.Activity, telepathy.client.Connection)'''
global text_chan, tubes_chan, DBUS_SERVICE
+ log.info( 'Setup for %s', activity )
if not DBUS_SERVICE:
DBUS_SERVICE = activity.get_bundle_id()
if not activity.get_shared():
log.error('Failed to share or join activity')
raise "Failure"
- bus_name, conn_path, channel_paths = activity._shared_activity.get_channels()
- _getConn()
-
- # Work out what our room is called and whether we have Tubes already
- room = None
- tubes_chan = None
- text_chan = None
- for channel_path in channel_paths:
- channel = telepathy.client.Channel(bus_name, channel_path)
- htype, handle = channel.GetHandle()
- if htype == telepathy.HANDLE_TYPE_ROOM:
- log.debug('Found our room: it has handle#%d "%s"',
- handle, conn.InspectHandles(htype, [handle])[0])
- room = handle
- ctype = channel.GetChannelType()
- if ctype == telepathy.CHANNEL_TYPE_TUBES:
- log.debug('Found our Tubes channel at %s', channel_path)
- tubes_chan = channel
- elif ctype == telepathy.CHANNEL_TYPE_TEXT:
- log.debug('Found our Text channel at %s', channel_path)
- text_chan = channel
-
- if room is None:
- log.error("Presence service didn't create a room")
- raise "Failure"
+ if hasattr( activity._shared_activity, 'telepathy_tubes_chan' ):
+ log.debug( '''Improved channel setup API available''' )
+ _getConn( activity )
+ conn = activity._shared_activity.telepathy_conn
+ tubes_chan = activity._shared_activity.telepathy_tubes_chan
+ text_chan = activity._shared_activity.telepathy_text_chan
+ else:
+ log.debug( '''Old-style setup API''' )
+ bus_name, conn_path, channel_paths = activity._shared_activity.get_channels()
+ _getConn( activity )
+
+ # Work out what our room is called and whether we have Tubes already
+ room = None
+ tubes_chan = None
+ text_chan = None
+ for channel_path in channel_paths:
+ log.debug( 'Testing channel path: %s', channel_path)
+ channel = telepathy.client.Channel(bus_name, channel_path)
+ htype, handle = channel.GetHandle()
+ log.debug( ' Handle Type: %s Handle: %s', htype, handle)
+ if htype == telepathy.HANDLE_TYPE_ROOM:
+ log.debug('Found our room: it has handle#%d "%s"',
+ handle, conn.InspectHandles(htype, [handle])[0])
+ room = handle
+ ctype = channel.GetChannelType()
+ if ctype == telepathy.CHANNEL_TYPE_TUBES:
+ log.debug('Found our Tubes channel at %s', channel_path)
+ tubes_chan = channel
+ elif ctype == telepathy.CHANNEL_TYPE_TEXT:
+ log.debug('Found our Text channel at %s', channel_path)
+ text_chan = channel
+
+ if room is None:
+ log.error("Presence service didn't create a room")
+ raise "Failure"
if text_chan is None:
log.error("Presence service didn't create a text channel")
raise "Failure"
@@ -196,16 +315,16 @@ setup(sugar.activity.Activity, telepathy.client.Connection)'''
tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
new_tube_cb)
+ log.info( 'Setup for %s complete', activity )
return (text_chan, tubes_chan)
-
def new_tube_cb(id, initiator, type, service, params, state):
log.debug("New_tube_cb called: %s %s %s" % (id, initiator, type))
if (type == telepathy.TUBE_TYPE_DBUS and service == DBUS_SERVICE):
if state == telepathy.TUBE_STATE_LOCAL_PENDING:
channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
- if hasattr(channel, 'AcceptDBusTube'):
- channel.AcceptDBusTube(id)
+ if hasattr( channel, 'AcceptDBusTube' ):
+ channel.AcceptDBusTube( id )
else:
channel.AcceptTube(id)
@@ -221,59 +340,98 @@ def _list_tubes_reply_cb(tubes):
for tube_info in tubes:
new_tube_cb(*tube_info)
-
def _list_tubes_error_cb(e):
log.error('ListTubes() failed: %s', e)
-
-def get_buddy(dbus_handle):
- """Get a Buddy from a handle."""
+def lookup_buddy( dbus_handle, callback, errback=None ):
+ """Do a lookup on the buddy information, callback with the information
+
+ Calls callback( buddy ) with the result of the lookup, or errback( error ) with
+ a dbus description of the error in the lookup process.
+
+ returns None
+ """
log.debug('Trying to find owner of handle %s...', dbus_handle)
cs_handle = instance().tube.bus_name_to_handle[dbus_handle]
log.debug('Trying to find my handle in %s...', cs_handle)
group = text_chan[telepathy.CHANNEL_INTERFACE_GROUP]
- log.debug('Calling GetSelfHandle')
- my_csh = group.GetSelfHandle()
- log.debug('My handle in that group is %s', my_csh)
- if my_csh == cs_handle:
- handle = conn.GetSelfHandle()
- log.debug('CS handle %s belongs to me, %s', cs_handle, handle)
- elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
- handle = group.GetHandleOwners([cs_handle])[0]
- log.debug('CS handle %s belongs to %s', cs_handle, handle)
- else:
- handle = cs_handle
- log.debug('non-CS handle %s belongs to itself', handle)
+ log.debug( 'Calling GetSelfHandle' )
+ if not errback:
+ def errback( error ):
+ log.error( """Failure retrieving handle for buddy lookup: %s""", error )
+ def with_my_csh( my_csh ):
+ log.debug('My handle in that group is %s', my_csh)
+ def _withHandle( handle ):
+ """process the results of the handle values"""
+ # XXX: we're assuming that we have Buddy objects for all contacts -
+ # this might break when the server becomes scalable.
+ pservice = _get_presence_service()
+ name, path = pservice.get_preferred_connection()
+ callback( pservice.get_buddy_by_telepathy_handle(name, path, handle) )
+ if my_csh == cs_handle:
+ conn.GetSelfHandle(reply_handler = _withHandle, error_handler=errback)
+ log.debug('CS handle %s belongs to me, looking up with GetSelfHandle', cs_handle)
+ elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ handle = group.GetHandleOwners([cs_handle])[0]
+ log.debug('CS handle %s belongs to %s', cs_handle, handle)
+ _withHandle( handle )
+ else:
+ handle = cs_handle
+ log.debug('non-CS handle %s belongs to itself', handle)
+ _withHandle( handle )
+ group.GetSelfHandle( reply_handler = with_my_csh, error_handler = errback)
- # XXX: we're assuming that we have Buddy objects for all contacts -
- # this might break when the server becomes scalable.
- pservice = _get_presence_service()
- name, path = pservice.get_preferred_connection()
- return pservice.get_buddy_by_telepathy_handle(name, path, handle)
+
+def get_buddy(dbus_handle):
+ """DEPRECATED: Get a Buddy from a handle
+
+ THIS API WAS NOT THREAD SAFE! It has been removed to avoid
+ extremely hard-to-debug failures in activities. Use lookup_buddy
+ instead!
+
+ Code that read:
+
+ get_buddy( handle )
+ doSomething( handle, buddy )
+ doSomethingElse( buddy )
+
+ Translates to:
+
+ def withBuddy( buddy ):
+ doSomething( handle, buddy )
+ doSomethingElse( buddy )
+ lookup_buddy( handle, callback=withBuddy )
+ """
+ raise RuntimeError(
+ """get_buddy is not thread safe and will crash your activity (hard). Use lookup_buddy."""
+ )
-def _get_presence_service():
+def _get_presence_service( ):
"""Attempt to retrieve the presence service (check for offline condition)
The presence service, when offline, has no preferred connection type,
so we check that before returning the object...
"""
- import sugar.presence.presenceservice
- pservice = sugar.presence.presenceservice.get_instance()
+ log.debug( """About to import sugar.presence.presenceservice""" )
try:
- name, path = pservice.get_preferred_connection()
- except (TypeError, ValueError), err:
- log.warn('Working in offline mode, cannot retrieve buddy information for %s: %s', handle, err)
- raise OfflineError("""Unable to retrieve buddy information, currently offline""")
- else:
- return pservice
-
+ log.debug( 'About to retrieve presence service instance' )
+ pservice = sugar.presence.presenceservice.get_instance()
+ try:
+ log.debug( ' Retrieved presence service instance: %s', pservice )
+ name, path = pservice.get_preferred_connection()
+ log.debug( ' Name = %s Path = %s', name, path )
+ except (TypeError,ValueError), err:
+ log.warn('Working in offline mode, cannot retrieve buddy information for %s: %s', handle, err )
+ raise OfflineError( """Unable to retrieve buddy information, currently offline""" )
+ else:
+ return pservice
+ except Exception, err:
+ log.error( """Failure in _get_presence_service: %s""", get_traceback( err ))
def instance(idx=0):
return pygametubes[idx]
-import eventwrap, pygame.event as PEvent
-
class PygameTube(ExportedGObject):
'''The object whose instance is shared across D-bus
@@ -283,36 +441,30 @@ class PygameTube(ExportedGObject):
'''
def __init__(self, tube, is_initiator, tube_id):
super(PygameTube, self).__init__(tube, DBUS_PATH)
- log.info('PygameTube init')
+ log.info( 'PygameTube init' )
self.tube = tube
self.is_initiator = is_initiator
self.entered = False
self.ordered_bus_names = []
- eventwrap.post(PEvent.Event(CONNECT, id=tube_id))
+ PEvent.post(PEvent.Event(CONNECT, id=tube_id))
if not self.is_initiator:
self.tube.add_signal_receiver(self.new_participant_cb, 'NewParticipants', DBUS_IFACE, path=DBUS_PATH)
self.tube.watch_participants(self.participant_change_cb)
self.tube.add_signal_receiver(self.broadcast_cb, 'Broadcast', DBUS_IFACE, path=DBUS_PATH, sender_keyword='sender')
- def participant_change_cb(self, added, removed):
- log.debug('participant_change_cb: %s %s', added, removed)
-
- def nick(buddy):
- if buddy is not None:
- return buddy.props.nick
- else:
- return 'Unknown'
+ def participant_change_cb(self, added, removed):
+ log.debug( 'participant_change_cb: %s %s', added, removed )
for handle, bus_name in added:
dbus_handle = self.tube.participants[handle]
self.ordered_bus_names.append(dbus_handle)
- eventwrap.post(PEvent.Event(PARTICIPANT_ADD, handle=dbus_handle))
+ PEvent.post(PEvent.Event(PARTICIPANT_ADD, handle=dbus_handle))
for handle in removed:
dbus_handle = self.tube.participants[handle]
self.ordered_bus_names.remove(dbus_handle)
- eventwrap.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle))
+ PEvent.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle))
if self.is_initiator:
if not self.entered:
@@ -337,11 +489,11 @@ class PygameTube(ExportedGObject):
@method(dbus_interface=DBUS_IFACE, in_signature='s', out_signature='', sender_keyword='sender')
def Tell(self, content, sender=None):
'''This is the targeted-message interface; called when a message is received that was sent directly to me.'''
- eventwrap.post(PEvent.Event(MESSAGE_UNI, handle=sender, content=content))
+ PEvent.post(PEvent.Event(MESSAGE_UNI, handle=sender, content=content))
def broadcast_cb(self, content, sender=None):
'''This is the Broadcast callback, fired when someone sends a Broadcast signal along the bus.'''
- eventwrap.post(PEvent.Event(MESSAGE_MULTI, handle=sender, content=content))
+ PEvent.post(PEvent.Event(MESSAGE_MULTI, handle=sender, content=content))
def new_participant_cb(self, new_bus_names):
'''This is the NewParticipants callback, fired when someone joins or leaves.'''
@@ -350,28 +502,22 @@ class PygameTube(ExportedGObject):
log.warn("ordered bus names out of sync with server, resyncing")
self.ordered_bus_names = new_bus_names
-
def send_to(handle, content=""):
'''Sends the given message to the given buddy identified by handle.'''
- log.debug('send_to: %s %s', handle, content)
+ log.debug( 'send_to: %s %s', handle, content )
remote_proxy = dbus_get_object(handle, DBUS_PATH)
remote_proxy.Tell(content, reply_handler=dbus_msg, error_handler=dbus_err)
-
def dbus_msg():
log.debug("async reply to send_to")
-
-
def dbus_err(e):
log.error("async error: %s" % e)
-
def broadcast(content=""):
'''Sends the given message to all participants.'''
- log.debug('Broadcast: %s', content)
+ log.debug( 'Broadcast: %s', content )
instance().Broadcast(content)
-
def my_handle():
'''Returns the handle of this user
@@ -379,36 +525,59 @@ def my_handle():
not yet got a unique ID assigned by the bus. You may need
to delay calling until you are sure you are connected.
'''
- log.debug('my handle')
+ log.debug( 'my handle' )
return instance().tube.get_unique_name()
-
def is_initiator():
'''Returns the handle of this user.'''
- log.debug('is initiator')
+ log.debug( 'is initiator' )
return instance().is_initiator
-
def get_participants():
'''Returns the list of active participants, in order of arrival.
List is maintained by the activity creator; if that person leaves it may not stay in sync.'''
- log.debug('get_participants')
+ log.debug( 'get_participants' )
try:
return instance().ordered_bus_names[:]
except IndexError, err:
- return [] # no participants yet, as we don't yet have a connection
+ return [] # no participants yet, as we don't yet have a connection
+def dbus_get_object(handle, path, warning=True):
+ '''Get a D-bus object from another participant
-def dbus_get_object(handle, path):
- '''Get a D-bus object from another participant.
+ Note: this *must* be called *only* from the GTK mainloop, calling
+ it from Pygame will cause crashes! If you are *sure* you only ever
+ want to call methods on this proxy from GTK, you can use
+ warning=False to silence the warning log message.
+ '''
+ if warning:
+ log.warn( 'Use of dbus_get_object is only safe from the GTK mainloop, use dbus_get_object_proxy instead: %s %s', handle, path )
+ return instance().tube.get_object(handle, path)
+
+def get_object(handle, path):
+ '''Get a D-BUS proxy object from another participant for use in Pygame
This is how you can communicate with other participants using
arbitrary D-bus objects without having to manage the participants
- yourself.
+ yourself. You can use the returned proxy's methods from Pygame,
+ with your callbacks occuring in the Pygame thread, rather than
+ in the DBUS/GTK event loop.
Simply define a D-bus class with an interface and path that you
choose; when you want a reference to the corresponding remote
object on a participant, call this method.
+
+ returns an olpcgames.dbusproxy.DBUSProxy( ) object wrapping
+ the DBUSProxy object.
+
+ The dbus_get_object_proxy name is deprecated
'''
- log.debug('dbus_get_object: %s %s', handle, path)
- return instance().tube.get_object(handle, path)
+ log.debug( 'DBUS get_object( %r %r )', handle, path )
+ from olpcgames import dbusproxy
+ return dbusproxy.DBUSProxy(
+ instance().tube.get_object( handle, path),
+ tube=instance().tube,
+ path=path
+ )
+
+dbus_get_object_proxy = get_object
diff --git a/olpcgames/mybutton.py b/olpcgames/mybutton.py
deleted file mode 100644
index 2d1677d..0000000
--- a/olpcgames/mybutton.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# mybutton.py A version of ActivityToolbarButton that hides the "Keep"
-# button.
-
-# Copyright (C) 2010 James D. Simmons
-#
-# 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 US
-import gtk
-import gconf
-
-from sugar.graphics.toolbarbox import ToolbarButton
-from sugar.activity.widgets import ActivityToolbar
-from sugar.graphics.xocolor import XoColor
-from sugar.graphics.icon import Icon
-from sugar.bundle.activitybundle import ActivityBundle
-
-def _create_activity_icon(metadata):
- if metadata.get('icon-color', ''):
- color = XoColor(metadata['icon-color'])
- else:
- client = gconf.client_get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
-
- from sugar.activity.activity import get_bundle_path
- bundle = ActivityBundle(get_bundle_path())
- icon = Icon(file=bundle.get_icon(), xo_color=color)
-
- return icon
-
-class MyActivityToolbarButton(ToolbarButton):
-
- def __init__(self, activity, **kwargs):
- toolbar = ActivityToolbar(activity, orientation_left=True)
- toolbar.share.hide()
- toolbar.stop.hide()
- toolbar.keep.hide()
-
- ToolbarButton.__init__(self, page=toolbar, **kwargs)
-
- icon = _create_activity_icon(activity.metadata)
- self.set_icon_widget(icon)
- icon.show()
diff --git a/olpcgames/pangofont.py b/olpcgames/pangofont.py
index 90e3dee..441dfd1 100644
--- a/olpcgames/pangofont.py
+++ b/olpcgames/pangofont.py
@@ -5,25 +5,61 @@ Depends on:
pygtk (to get the pango context)
pycairo (for the pango rendering context)
python-pango (obviously)
- pygame (obviously)
+ numpy
+ (pygame)
+
+As soon as you import this module you have loaded *all* of the above.
+You can still use pygame.font until you decide to call install(), which
+will replace pygame.font with this module.
+
+Notes:
+
+ * no ability to load TTF files, PangoFont uses the font files registered
+ with GTK/X to render graphics, it cannot load an arbitrary TTF file.
+ Most non-Sugar Pygame games use bundled TTF files, which means
+ that you will likely need at least some changes to your font handling.
+
+ Note, however, that the Pygame Font class is available to load the TTF
+ files, so if you don't want to take advantage of PangoFont for already
+ written code, but want to use it for "system font" operations, you can
+ mix the two.
+
+ * metrics are missing, Pango can provide the information, but the more
+ involved metrics system means that translating to the simplified model
+ in Pygame has as of yet not been accomplished.
+
+ * better support for "exotic" languages and scripts (which is why we use it)
+
+The main problem with SDL_ttf is that it doesn't handle internationalization
+nearly as well as Pango (in fact, pretty much nothing does). However, it is
+fairly fast and it has a rich interface. You should avoid fonts where possible,
+prerender using Pango for internationalizable text, and use Pango or SDL_ttf
+for text that really needs to be rerendered each frame. (Use SDL_ttf if profiling
+demonstrates that performance is poor with Pango.)
+
+Note:
+ Font -- is the original Pygame Font class, which allows you to load
+ fonts from TTF files/filenames
+ PangoFont -- is the Pango-specific rendering engine which allows
+ for the more involved cross-lingual rendering operations.
"""
import pango
import logging
-import cairo
import pangocairo
import pygame.rect, pygame.image
import gtk
import struct
from pygame import surface
+from pygame.font import Font
from olpcgames import _cairoimage
log = logging.getLogger( 'olpcgames.pangofont' )
-#log.setLevel( logging.DEBUG )
+##log.setLevel( logging.DEBUG )
# Install myself on top of pygame.font
def install():
"""Replace Pygame's font module with this module"""
- log.info('installing')
+ log.info( 'installing' )
from olpcgames import pangofont
import pygame
pygame.font = pangofont
@@ -62,13 +98,16 @@ class PangoFont(object):
if family is not None:
fd.set_family(family)
if size is not None:
- fd.set_size(size*1000)
+ log.debug( 'Pre-conversion size: %s', size )
+ size = int(size*1024)
+ log.debug( 'Font size: %s', size, )
+ fd.set_size(size) # XXX magic number, pango's scaling
self.fd = fd
self.set_bold( bold )
self.set_italic( italic )
self.set_underline( underline )
- def render(self, text, antialias=True, color=(255,255,255), background=None):
+ def render(self, text, antialias=True, color=(255,255,255), background=None ):
"""Render the font onto a new Surface and return it.
We ignore 'antialias' and use system settings.
@@ -81,25 +120,15 @@ class PangoFont(object):
returns a pygame image instance
"""
- log.info('render: %r, antialias = %s, color=%s, background=%s', text, antialias, color, background)
-
- # create layout
- layout = pango.Layout(gtk.gdk.pango_context_get())
- layout.set_font_description(self.fd)
- if self.underline:
- attrs = layout.get_attributes()
- if not attrs:
- attrs = pango.AttrList()
- attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767))
- layout.set_attributes(attrs)
- layout.set_text(text)
+ log.info( 'render: %r, antialias = %s, color=%s, background=%s', text, antialias, color, background )
+ layout = self._createLayout( text )
# determine pixel size
(logical, ink) = layout.get_pixel_extents()
ink = pygame.rect.Rect(ink)
# Create a new Cairo ImageSurface
- csrf,cctx = _cairoimage.newContext(ink.w, ink.h)
+ csrf,cctx = _cairoimage.newContext( ink.w, ink.h )
cctx = pangocairo.CairoContext(cctx)
# Mangle the colors on little-endian machines. The reason for this
@@ -110,13 +139,13 @@ class PangoFont(object):
# render onto it
if background is not None:
- background = _cairoimage.mangle_color(background)
+ background = _cairoimage.mangle_color( background )
cctx.set_source_rgba(*background)
cctx.paint()
- log.debug('incoming color: %s', color)
- color = _cairoimage.mangle_color(color)
- log.debug(' translated color: %s', color)
+ log.debug( 'incoming color: %s', color )
+ color = _cairoimage.mangle_color( color )
+ log.debug( ' translated color: %s', color )
cctx.new_path()
cctx.layout_path(layout)
@@ -124,7 +153,7 @@ class PangoFont(object):
cctx.fill()
# Create and return a new Pygame Image derived from the Cairo Surface
- return _cairoimage.asImage(csrf)
+ return _cairoimage.asImage( csrf )
def set_bold( self, bold=True):
"""Set our font description's weight to "bold" or "normal"
@@ -132,43 +161,78 @@ class PangoFont(object):
bold -- boolean, whether to set the value to "bold" weight or not
"""
if bold:
- self.set_weight(self.WEIGHT_BOLD)
+ self.set_weight( self.WEIGHT_BOLD )
else:
- self.set_weight(self.WEIGHT_NORMAL)
- def set_weight(self, weight):
+ self.set_weight( self.WEIGHT_NORMAL )
+ def set_weight( self, weight ):
"""Explicitly set our pango-style weight value"""
- self.fd.set_weight(weight)
+ self.fd.set_weight( weight )
return self.get_weight()
- def get_weight(self):
+ def get_weight( self ):
"""Explicitly get our pango-style weight value"""
return self.fd.get_weight()
- def get_bold(self):
+ def get_bold( self ):
"""Return whether our font's weight is bold (or above)"""
return self.fd.get_weight() >= pango.WEIGHT_BOLD
- def set_italic(self, italic=True):
+ def set_italic( self, italic=True ):
"""Set our "italic" value (style)"""
if italic:
- self.set_style(self.STYLE_ITALIC)
+ self.set_style( self.STYLE_ITALIC )
else:
- self.set_style(self.STYLE_NORMAL)
- def set_style(self, style):
+ self.set_style( self.STYLE_NORMAL )
+ def set_style( self, style ):
"""Set our font description's pango-style"""
- self.fd.set_style(style)
+ self.fd.set_style( style )
return self.fd.get_style()
- def get_style(self):
+ def get_style( self ):
"""Get our font description's pango-style"""
return self.fd.get_style()
- def get_italic(self):
+ def get_italic( self ):
"""Return whether we are currently italicised"""
- return self.fd.get_style() == self.STYLE_ITALIC # what about oblique?
+ return self.fd.get_style() == self.STYLE_ITALIC # what about oblique?
- def set_underline(self, underline=True):
+ def set_underline( self, underline=True ):
"""Set our current underlining properly"""
self.underline = underline
- def get_underline(self):
+ def get_underline( self ):
+ """Retrieve our current underline setting"""
return self.underline
+ def _createLayout( self, text ):
+ """Produces a Pango layout describing this text in this font"""
+ # create layout
+ layout = pango.Layout(gtk.gdk.pango_context_get())
+ layout.set_font_description(self.fd)
+ if self.underline:
+ attrs = layout.get_attributes()
+ if not attrs:
+ attrs = pango.AttrList()
+ attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767))
+ layout.set_attributes( attrs )
+ layout.set_text(text)
+ return layout
+
+ def size( self, text ):
+ """Determine space required to render given text
+
+ returns tuple of (width,height)
+ """
+ layout = self._createLayout( text )
+ (logical, ink) = layout.get_pixel_extents()
+ ink = pygame.rect.Rect(ink)
+ return (ink.width,ink.height)
+
+## def get_linesize( self ):
+## """Determine inter-line spacing for the font"""
+## font = self.get_context().load_font( self.fd )
+## metrics = font.get_metrics()
+## return pango.PIXELS( metrics.get_ascent() )
+## def get_height( self ):
+## def get_ascent( self ):
+## def get_descent( self ):
+
+
class SysFont(PangoFont):
"""Construct a PangoFont from a font description (name), size in pixels,
bold, and italic designation. Similar to SysFont from Pygame."""
@@ -184,17 +248,20 @@ class SysFont(PangoFont):
# originally defined a new class, no reason for that...
NotImplemented = NotImplementedError
-class Font(PangoFont):
- """Abstract class, do not use"""
- def __init__(self, *args, **kwargs):
- raise NotImplementedError("PangoFont doesn't support Font directly, use SysFont or .fontByDesc")
-
def match_font(name,bold=False,italic=False):
"""Stub, does not work, use fontByDesc instead"""
raise NotImplementedError("PangoFont doesn't support match_font directly, use SysFont or .fontByDesc")
def fontByDesc(desc="",bold=False,italic=False):
"""Constructs a FontDescription from the given string representation.
+
+The format of the fontByDesc string representation is passed directly
+to the pango.FontDescription constructor and documented at:
+
+ http://www.pygtk.org/docs/pygtk/class-pangofontdescription.html#constructor-pangofontdescription
+
+Bold and italic are provided as a convenience.
+
The format of the string representation is:
"[FAMILY-LIST] [STYLE-OPTIONS] [SIZE]"
@@ -274,6 +341,6 @@ def stdcolor(color):
else:
raise TypeError("What sort of color is this: %s" % (color,))
return [_fixColorBase(x) for x in fixlen(color)]
-def _fixColorBase(v):
+def _fixColorBase( v ):
"""Return a properly clamped colour in floating-point space"""
return max((0,min((v,255.0))))/255.0
diff --git a/olpcgames/pausescreen.py b/olpcgames/pausescreen.py
index 95f1a7a..113a0ea 100644
--- a/olpcgames/pausescreen.py
+++ b/olpcgames/pausescreen.py
@@ -7,12 +7,39 @@ we have more involved activities using the code.
We use svgsprite to render a graphic which is stored in the
olpcgames data directory over a dimmed version of the current
screen contents.
+
+_LAST_EVENT_TIME -- tracks the last time that we saw an event
+ come across the wire.
"""
import logging
-log = logging.getLogger('olpcgames.pausescreen')
+log = logging.getLogger( 'olpcgames.pausescreen' )
import pygame
from pygame import sprite
+_LAST_EVENT_TIME = 0
+
+def _set_last_event_time( time=None ):
+ """Set time as the last event time
+
+ time -- if None, pygame.time.get_ticks() is used
+
+ returns time set
+ """
+ global _LAST_EVENT_TIME
+ if time is None:
+ time = pygame.time.get_ticks()
+ _LAST_EVENT_TIME = time
+ return time
+
+def last_event_time( ):
+ """Return the duration since last event for pausing operations
+
+ returns time in seconds
+ """
+ global _LAST_EVENT_TIME
+ return (pygame.time.get_ticks() - _LAST_EVENT_TIME)/1000.
+
+
def get_events( sleep_timeout = 10, pause=None, **args ):
"""Retrieve the set of pending events or sleep
@@ -21,7 +48,7 @@ def get_events( sleep_timeout = 10, pause=None, **args ):
by taking the current screen and modifying it in some way. Defaults
to pauseScreen in this module. If you return nothing from this
function then no restoration or display-flipping will occur
- *args -- if present, passed to pause to configuration operation (e.g.
+ *args -- if present, passed to 'pause' to configuration operation (e.g.
to specify a different overlaySVG file)
returns set of pending events (potentially empty)
@@ -30,25 +57,25 @@ def get_events( sleep_timeout = 10, pause=None, **args ):
pause = pauseScreen
events = pygame.event.get( )
if not events:
- log.info('No events in queue')
+ log.info( 'No events in queue' )
old_screen = None
- if hasattr(pygame.event, 'last_event_time') and pygame.event.last_event_time() > sleep_timeout:
+ if last_event_time() > sleep_timeout:
# we've been waiting long enough, go to sleep visually
- log.warn('Pausing activity after %s with function %s', sleep_timeout, pause)
+ log.warn( 'Pausing activity after %s with function %s', sleep_timeout, pause )
old_screen = pause( )
if old_screen:
pygame.display.flip()
# now we wait until there *are* some events (efficiently)
# and retrieve any extra events that are waiting...
events = [ pygame.event.wait() ] + pygame.event.get()
- log.warn('Activity restarted')
+ log.warn( 'Activity restarted')
if old_screen:
- restoreScreen(old_screen)
- else:
- log.info('Not running under OLPCGames')
+ restoreScreen( old_screen )
+ if events:
+ _set_last_event_time()
return events
-def pauseScreen(overlaySVG=None):
+def pauseScreen( overlaySVG=None ):
"""Display a "Paused" screen and suspend
This default implementation will not do anything to shut down your
@@ -69,7 +96,7 @@ def pauseScreen(overlaySVG=None):
)
pause_sprite.rect.center = screen.get_rect().center
group = sprite.RenderUpdates( )
- group.add(pause_sprite)
+ group.add( pause_sprite )
# dim the screen and display the 'paused' message in the center.
BLACK = (0,0,0)
@@ -79,10 +106,10 @@ def pauseScreen(overlaySVG=None):
screen.fill(BLACK)
screen.blit(dimmed, (0,0))
- group.draw(screen)
+ group.draw( screen )
return old_screen
-def restoreScreen(old_screen):
+def restoreScreen( old_screen ):
"""Restore the original screen and return"""
screen = pygame.display.get_surface()
screen.blit(old_screen, (0,0))
diff --git a/olpcgames/svgsprite.py b/olpcgames/svgsprite.py
index 2c53178..ad247dd 100644
--- a/olpcgames/svgsprite.py
+++ b/olpcgames/svgsprite.py
@@ -1,10 +1,16 @@
"""RSVG/Cairo-based rendering of SVG into Pygame Images"""
-from pygame import sprite
+from pygame import sprite, Rect
from olpcgames import _cairoimage
-import cairo, rsvg
class SVGSprite( sprite.Sprite ):
- """Sprite class which renders SVG source-code as a Pygame image"""
+ """Sprite class which renders SVG source-code as a Pygame image
+
+ Note:
+
+ Currently this sprite class is a bit over-engineered, it gets in the way
+ if you want to, e.g. animate among a number of SVG drawings, as it
+ assumes that setSVG will always set a single SVG file for rendering.
+ """
rect = image = None
resolution = None
def __init__(
@@ -30,7 +36,7 @@ class SVGSprite( sprite.Sprite ):
width,height = self.size
else:
width,height = None,None
- self.image = self._render( width,height )
+ self.image = self._render( width,height ).convert_alpha()
rect = self.image.get_rect()
if self.rect:
rect.move( self.rect ) # should let something higher-level do that...
@@ -38,6 +44,7 @@ class SVGSprite( sprite.Sprite ):
def _render( self, width, height ):
"""Render our SVG to a Pygame image"""
+ import rsvg
handle = rsvg.Handle( data = self.svg )
originalSize = (width,height)
scale = 1.0
@@ -66,4 +73,12 @@ class SVGSprite( sprite.Sprite ):
handle.render_cairo( ctx )
return _cairoimage.asImage( csrf )
return None
-
+ def copy( self ):
+ """Create a copy of this sprite without reloading the svg image"""
+ result = self.__class__(
+ size = self.size
+ )
+ result.image = self.image
+ result.rect = Rect(self.rect)
+ result.resolution = self.resolution
+ return result
diff --git a/olpcgames/textsprite.py b/olpcgames/textsprite.py
new file mode 100644
index 0000000..7663630
--- /dev/null
+++ b/olpcgames/textsprite.py
@@ -0,0 +1,40 @@
+"""Simple Sprite sub-class that renders via a PangoFont"""
+from pygame import sprite
+from olpcgames import pangofont
+
+class TextSprite( sprite.Sprite ):
+ """Sprite with a simple text renderer"""
+ image = rect = text = color = background = None
+ def __init__( self, text=None, family=None, size=None, bold=False, italic=False, color=None, background=None ):
+ super( TextSprite, self ).__init__( )
+ self.font = pangofont.PangoFont( family=family, size=size, bold=bold, italic=italic )
+ self.set_color( color )
+ self.set_background( background )
+ self.set_text( text )
+ def set_text( self, text ):
+ """Set our text string and render to a graphic"""
+ self.text = text
+ self.render( )
+ def set_color( self, color =None):
+ """Set our rendering colour (default white)"""
+ self.color = color or (255,255,255)
+ self.render()
+ def set_background( self, color=None ):
+ """Set our background color, default transparent"""
+ self.background = color
+ self.render()
+ def render( self ):
+ """Render our image and rect (or None,None)
+
+ After a render you will need to move the rect member to the
+ correct location on the screen.
+ """
+ if self.text:
+ self.image = self.font.render( self.text, color = self.color, background = self.background )
+ currentRect = self.rect
+ self.rect = self.image.get_rect()
+ if currentRect:
+ self.rect.center = currentRect.center
+ else:
+ self.rect = None
+ self.image = None
diff --git a/olpcgames/util.py b/olpcgames/util.py
index f4ecbf0..49a23b0 100644
--- a/olpcgames/util.py
+++ b/olpcgames/util.py
@@ -58,11 +58,22 @@ def get_traceback(error):
util.get_traceback( err ),
)
"""
- exception = str(error)
- file = cStringIO.StringIO()
- try:
- traceback.print_exc( limit=10, file = file )
- exception = file.getvalue()
- finally:
- file.close()
- return exception
+ if error is None:
+ error = []
+ for (f,l,func,statement) in traceback.extract_stack()[:-2]:
+ if statement:
+ statement = ': %s'%( statement, )
+ if func:
+ error.append( '%s.%s (%s)%s'%( f,func,l, statement))
+ else:
+ error.append( '%s (%s)%s'%( f,l, statement))
+ return "\n".join( error )
+ else:
+ exception = str(error)
+ file = cStringIO.StringIO()
+ try:
+ traceback.print_exc( limit=10, file = file )
+ exception = file.getvalue()
+ finally:
+ file.close()
+ return exception
diff --git a/olpcgames/video.py b/olpcgames/video.py
index 0cf9ac9..032aa13 100644
--- a/olpcgames/video.py
+++ b/olpcgames/video.py
@@ -1,11 +1,18 @@
-"""Video widget for displaying a gstreamer pipe"""
+"""Video widget for displaying a gstreamer pipe
+
+Note: currently this module is not all that elegant or useful,
+we need a better recipe for using and working with Video
+under OLPCGames.
+"""
import logging
log = logging.getLogger( 'olpcgames.video' )
#log.setLevel( logging.INFO )
import os
import signal
import pygame
+import weakref
import olpcgames
+from olpcgames import _gtkmain
import pygtk
pygtk.require('2.0')
@@ -79,6 +86,7 @@ class PygameWidget( object ):
window_id = int(os.environ['SDL_WINDOWID'])
self.window_id = window_id
self._imagesink = None
+ #self._holder = _gtkmain.Holder()
def set_sink( self, sink ):
"""Set up our gst sink"""
log.info( 'Setting sink: %s', sink )
@@ -149,7 +157,7 @@ if __name__ == "__main__":
display.flip()
pgw = PygameWidget( )
- p = Player( pgw )
+ p = Player( pgw, pipe_desc=Player.test_pipe_desc )
p.play()
clock = pygame.time.Clock()
--
1.7.9.1
More information about the Sugar-devel
mailing list