Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIgnacio Rodriguez <ignacio@sugarlabs.org>2013-12-23 23:01:23 (GMT)
committer Ignacio Rodriguez <ignacio@sugarlabs.org>2013-12-23 23:01:23 (GMT)
commit0334288a7a2fa1292c9749fa9bb18aa75de910f1 (patch)
treeaeb6b3a3950e7382e3cc5c6a13cda99728e3332a
parente0624bc74a623697860cefaa04cc440cbc24bcdc (diff)
port to gtk3 and sugargame a lot of bugs.
-rwxr-xr-xactivity.py70
-rw-r--r--game.py81
-rw-r--r--player.py2
-rw-r--r--sugargame/__init__.py7
-rw-r--r--sugargame/data/__init__.py35
-rw-r--r--sugargame/data/sleeping_svg.py61
-rw-r--r--sugargame/dbusproxy.py89
-rw-r--r--sugargame/eventwrap.py417
-rw-r--r--sugargame/mesh.py435
-rw-r--r--sugargame/mesh.pycbin0 -> 15664 bytes
-rw-r--r--sugargame/svgsprite.py88
-rw-r--r--sugargame/util.py77
12 files changed, 1276 insertions, 86 deletions
diff --git a/activity.py b/activity.py
index 68d73d3..916569c 100755
--- a/activity.py
+++ b/activity.py
@@ -1,61 +1,47 @@
# -*- coding: utf-8 -*-
import logging
-import olpcgames
+import sugargame.canvas
+from sugargame import mesh
import pygame
-import gtk
+from gi.repository import Gtk
-from olpcgames import mesh
-from olpcgames import util
-
-from sugar.activity.widgets import ActivityToolbarButton
-from sugar.activity.widgets import StopButton
-from sugar.graphics.toolbarbox import ToolbarBox
-from sugar.graphics.toolbutton import ToolButton
+from sugar3.activity import activity
+from sugar3.activity.widgets import ActivityToolbarButton
+from sugar3.activity.widgets import StopButton
+from sugar3.graphics.toolbarbox import ToolbarBox
+from sugar3.graphics.toolbutton import ToolButton
from gettext import gettext as _
+import game
-class MazeActivity(olpcgames.PyGameActivity):
- game_name = 'game'
- game_title = _('Maze')
- game_size = None # Let olpcgames pick a nice size for us
+class MazeActivity(activity.Activity):
def __init__(self, handle):
super(MazeActivity, self).__init__(handle)
+ self.build_toolbar()
+
+ # Build the Pygame canvas.
+ self._pygamecanvas = sugargame.canvas.PygameCanvas(self)
+ self.set_canvas(self._pygamecanvas)
- # This code was copied from olpcgames.activity.PyGameActivity
def shared_cb(*args, **kwargs):
- logging.info('shared: %s, %s', args, kwargs)
- try:
- mesh.activity_shared(self)
- except Exception, err:
- logging.error('Failure signaling activity sharing'
- 'to mesh module: %s', util.get_traceback(err))
- else:
- logging.info('mesh activity shared message sent,'
- ' trying to grab focus')
- try:
- self._pgc.grab_focus()
- except Exception, err:
- logging.warn('Focus failed: %s', err)
- else:
- logging.info('asserting focus')
- assert self._pgc.is_focus(), \
- 'Did not successfully set pygame canvas focus'
- logging.info('callback finished')
+ mesh.activity_shared(self)
+ self._pygamecanvas.grab_focus()
def joined_cb(*args, **kwargs):
- logging.info('joined: %s, %s', args, kwargs)
mesh.activity_joined(self)
- self._pgc.grab_focus()
+ self._pygamecanvas.grab_focus()
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()
+ self.game = game.MazeGame()
+ self._canvas.run_pygame(self.game.run)
+ self._pygamecanvas.grab_focus()
+
def build_toolbar(self):
"""Build our Activity toolbar for the Sugar system."""
@@ -64,7 +50,7 @@ class MazeActivity(olpcgames.PyGameActivity):
toolbar_box.toolbar.insert(activity_button, 0)
activity_button.show()
- separator = gtk.SeparatorToolItem()
+ separator = Gtk.SeparatorToolItem()
toolbar_box.toolbar.insert(separator, -1)
separator.show()
@@ -78,7 +64,7 @@ class MazeActivity(olpcgames.PyGameActivity):
harder_button.connect('clicked', self._harder_button_cb)
toolbar_box.toolbar.insert(harder_button, -1)
- separator = gtk.SeparatorToolItem()
+ separator = Gtk.SeparatorToolItem()
separator.props.draw = False
separator.set_size_request(0, -1)
separator.set_expand(True)
@@ -100,9 +86,9 @@ class MazeActivity(olpcgames.PyGameActivity):
pygame.quit()
def _easier_button_cb(self, button):
- pygame.event.post(olpcgames.eventwrap.Event(
- pygame.USEREVENT, action='easier_button'))
+ pygame.event.post(pygame.event.Event(pygame.USEREVENT,
+ action='easier_button'))
def _harder_button_cb(self, button):
- pygame.event.post(olpcgames.eventwrap.Event(
- pygame.USEREVENT, action='harder_button'))
+ pygame.event.post(pygame.event.Event(pygame.USEREVENT,
+ action='harder_button'))
diff --git a/game.py b/game.py
index 50d2d6f..d691535 100644
--- a/game.py
+++ b/game.py
@@ -26,33 +26,26 @@
import sys
import time
import json
-import gtk
+import os
+from gi.repository import Gtk
+from gi.repository import Gdk
import pygame
-import olpcgames
+import sugargame
import logging
logging.basicConfig()
log = logging.getLogger('Maze')
log.setLevel(logging.DEBUG)
-import olpcgames.pausescreen as pausescreen
-import olpcgames.mesh as mesh
-from olpcgames.util import get_bundle_path
-from sugar.presence import presenceservice
-from sugar.graphics.style import GRID_CELL_SIZE
+import sugargame.mesh as mesh
+from sugar3.activity.activity import get_bundle_path
+from sugar3.presence import presenceservice
+from sugar3.graphics.style import GRID_CELL_SIZE
bundlepath = get_bundle_path()
presenceService = presenceservice.get_instance()
-# # MakeBot on OS X - useful for prototyping with pygame
-# # http://stratolab.com/misc/makebot/
-# sys.path.append("/Applications/MakeBot-1.4/site-packages")
-# import pygame
-# pygame.init()
-# bundlepath = ""
-# canvas_size = (1200,825)
-
from maze import Maze
from player import Player
@@ -80,7 +73,7 @@ class MazeGame:
GOAL_COLOR = (0x00, 0xff, 0x00)
WIN_COLOR = (0xff, 0xff, 0x00)
- def __init__(self, screen):
+ def first_run(self):
# note what time it was when we first launched
self.game_start_time = time.time()
@@ -99,20 +92,6 @@ class MazeGame:
# keep a list of all players, local and remote,
self.allplayers = [] + self.localplayers
- self.screen = screen
- 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
- data = {'seed': int(time.time()),
- 'width': int(9 * self.aspectRatio),
- 'height': 9}
-
- log.debug('Starting the game with: %s', data)
- self.maze = Maze(**data)
- self.reset()
-
self.frame = 0
self.font = pygame.font.Font(None, 30)
@@ -135,13 +114,13 @@ class MazeGame:
pygame.K_KP1: (2, pygame.K_RIGHT)
}
- gtk.gdk.screen_get_default().connect('size-changed',
+ Gdk.Screen.get_default().connect('size-changed',
self.__configure_cb)
def __configure_cb(self, event):
''' Screen size has changed '''
- width = gtk.gdk.screen_width()
- height = gtk.gdk.screen_height() - GRID_CELL_SIZE
+ width = Gdk.Screen.width()
+ height = Gdk.Screen.height() - GRID_CELL_SIZE
self.aspectRatio = width / float(height)
pygame.display.set_mode((width, height), pygame.RESIZABLE)
@@ -349,14 +328,14 @@ class MazeGame:
elif hasattr(event, 'action') and event.action == 'easier_button':
self.easier()
# process file save / restore events
- elif event.code == olpcgames.FILE_READ_REQUEST:
+ elif event.code == sugargame.FILE_READ_REQUEST:
log.debug('Loading the state of the game...')
state = json.loads(event.metadata['state'])
log.debug('Loaded data: %s', state)
self.maze = Maze(**state)
self.reset()
return True
- elif event.code == olpcgames.FILE_WRITE_REQUEST:
+ elif event.code == sugargame.FILE_WRITE_REQUEST:
log.debug('Saving the state of the game...')
data = {'seed': self.maze.seed,
'width': self.maze.width,
@@ -456,28 +435,43 @@ class MazeGame:
def run(self):
"""Run the main loop of the game."""
# lets draw once before we enter the event loop
+ self.first_run()
+
+ self.screen = pygame.display.get_surface()
+ canvas_size = self.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
+ data = {'seed': int(time.time()),
+ 'width': int(9 * self.aspectRatio),
+ 'height': 9}
+
+ log.debug('Starting the game with: %s', data)
+ self.maze = Maze(**data)
+ self.reset()
clock = pygame.time.Clock()
pygame.display.flip()
+ my_cursor = os.path.join(get_bundle_path(), 'my_cursor.xbm')
+ my_cursor_mask = os.path.join(get_bundle_path(), 'my_cursor_mask.xbm')
while self.running:
- clock.tick(25)
+ clock.tick(120)
- a, b, c, d = pygame.cursors.load_xbm('my_cursor.xbm',
- 'my_cursor_mask.xbm')
+ a, b, c, d = pygame.cursors.load_xbm(my_cursor,
+ my_cursor_mask)
pygame.mouse.set_cursor(a, b, c, d)
self.frame += 1
- # process all queued events
- for event in pausescreen.get_events(sleep_timeout=30):
+ for event in pygame.event.get():
self.processEvent(event)
self.animate()
self.draw()
pygame.display.update()
- # don't animate faster than about 20 frames per second
- # this keeps the speed reasonable and limits cpu usage
- clock.tick(25)
+
def harder(self):
"""Make a new maze that is harder than the current one."""
@@ -547,6 +541,7 @@ class MazeGame:
return
# compute the size of the tiles given the screen size, etc.
+ self.screen = pygame.display.get_surface()
size = self.screen.get_size()
self.tileSize = min(size[0] / self.maze.width,
size[1] / self.maze.height)
diff --git a/player.py b/player.py
index f9a3452..a2f3009 100644
--- a/player.py
+++ b/player.py
@@ -25,7 +25,7 @@
import pygame
import unicodedata
-from olpcgames.util import get_bundle_path
+from sugar3.activity.activity import get_bundle_path
bundlepath = get_bundle_path()
diff --git a/sugargame/__init__.py b/sugargame/__init__.py
index 439eb0c..4eb1345 100644
--- a/sugargame/__init__.py
+++ b/sugargame/__init__.py
@@ -1 +1,8 @@
__version__ = '1.1'
+(CAMERA_LOAD, CAMERA_LOAD_FAIL, CONNECT, PARTICIPANT_ADD, PARTICIPANT_REMOVE,
+ MESSAGE_UNI, MESSAGE_MULTI) = range(25, 32)
+
+(FILE_READ_REQUEST, FILE_WRITE_REQUEST) = range(2 ** 16, 2 ** 16 + 2)
+
+ACTIVITY = None
+widget = WIDGET = None \ No newline at end of file
diff --git a/sugargame/data/__init__.py b/sugargame/data/__init__.py
new file mode 100644
index 0000000..68e2a69
--- /dev/null
+++ b/sugargame/data/__init__.py
@@ -0,0 +1,35 @@
+"""Design-time __init__.py for resourcepackage
+
+This is the scanning version of __init__.py for your
+resource modules. You replace it with a blank or doc-only
+init when ready to release.
+"""
+try:
+ __file__
+except NameError:
+ pass
+else:
+ import os
+ if os.path.splitext(os.path.basename(__file__))[0] == "__init__":
+ try:
+ from resourcepackage import package, defaultgenerators
+ generators = defaultgenerators.generators.copy()
+
+ ### CUSTOMISATION POINT
+ ## import specialised generators here, such as for wxPython
+ #from resourcepackage import wxgenerators
+ #generators.update(wxgenerators.generators)
+ except ImportError:
+ pass
+ else:
+ package = package.Package(
+ packageName=__name__,
+ directory=os.path.dirname(os.path.abspath(__file__)),
+ generators=generators,
+ )
+ package.scan(
+ ### CUSTOMISATION POINT
+ ## force true -> always re-loads from external files, otherwise
+ ## only reloads if the file is newer than the generated py file.
+ # force=1,
+ )
diff --git a/sugargame/data/sleeping_svg.py b/sugargame/data/sleeping_svg.py
new file mode 100644
index 0000000..4a5926c
--- /dev/null
+++ b/sugargame/data/sleeping_svg.py
@@ -0,0 +1,61 @@
+# -*- coding: ISO-8859-1 -*-
+"""Resource sleeping_svg (from file sleeping.svg)"""
+# written by resourcepackage: (1, 0, 1)
+source = 'sleeping.svg'
+package = 'sugargame.data'
+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/sugargame/dbusproxy.py b/sugargame/dbusproxy.py
new file mode 100644
index 0000000..7c0204d
--- /dev/null
+++ b/sugargame/dbusproxy.py
@@ -0,0 +1,89 @@
+"""Spike test for a safer networking system for DBUS-based objects"""
+
+from sugargame import eventwrap, util
+from dbus import proxies
+
+
+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):
+ 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
+ """
+ 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):
+ self.__proxy = proxy
+ self.__tube = tube
+ self.__path = path
+
+ def __call__(self, *args, **named):
+ """Perform the asynchronous call"""
+ 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"""
+ pass
+
+
+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"""
+ 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/sugargame/eventwrap.py b/sugargame/eventwrap.py
new file mode 100644
index 0000000..fe3d33d
--- /dev/null
+++ b/sugargame/eventwrap.py
@@ -0,0 +1,417 @@
+"""Provides substitute for Pygame's "event" module using gtkEvent
+
+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.
+
+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.
+
+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 Queue
+import thread
+import threading
+from sugargame import util
+
+from pygame.event import Event, pump as pygame_pump, get as pygame_get
+
+
+class Event(object):
+ """Mock pygame events"""
+ 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.
+ """
+ self.__lock = threading.Event()
+ self.__lock.wait()
+
+ def retire(self):
+ """Block the GTK event loop until this event is processed"""
+ try:
+ self.__lock.set()
+ except AttributeError:
+ 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:
+ pass
+
+
+_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:
+ 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
+
+
+def install():
+ """Installs this module (eventwrap) as an in-place replacement for the
+ pygame.event module.
+
+ Use install() when you need to interact with Pygame code written
+ without reference to the olpcgames wrapper mechanisms to have the
+ code use this module's event queue.
+
+ XXX Really, use it everywhere you want to use olpcgames, as olpcgames
+ 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).
+ """
+ from sugargame import eventwrap
+ import pygame
+ pygame.event = eventwrap
+ import sys
+ sys.modules["pygame.event"] = eventwrap
+
+
+# Event 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:
+ 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() # 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:
+ 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()
+ """
+ pygame_pump()
+ _releaseEvents()
+
+
+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:
+ 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
+
+ pygameEvents = pygame_get()
+ if pygameEvents:
+ eventlist.extend(pygameEvents)
+ return _recordEvents(eventlist)
+
+
+def poll():
+ """
+ Get the next pending event if exists. Otherwise, return pygame.NOEVENT.
+ """
+ pump()
+ try:
+ result = g_events.get(block=False)
+ return _recordEvents([result])[0]
+ except Queue.Empty:
+ return Event(pygame.NOEVENT)
+
+
+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. timeout
+ is an OLPCGames-specific extension.
+ """
+ pump()
+ try:
+ result = None
+ result = g_events.get(block=True, timeout=timeout)
+ try:
+ return _recordEvents([result])[0]
+ except IndexError:
+ return Event(type=pygame.NOEVENT)
+ except Queue.Empty:
+ return None
+
+
+def peek(types=None):
+ """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():
+ """Clears the entire pending queue of events
+
+ Rarely used
+ """
+ try:
+ discarded = []
+ while True:
+ 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
+ # you set_blocked(none).
+ [g_blocked.add(x) for x in makeseq(item)]
+ finally:
+ 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:
+ # Allow all events when you set_allowed(none). Strange, eh?
+ # Pygame is a wonderful API.
+ g_blocked.clear()
+ else:
+ [g_blocked.remove(x) for x in makeseq(item)]
+ finally:
+ g_blockedlock.release()
+
+
+def get_blocked(*args, **kwargs):
+ g_blockedlock.acquire()
+ try:
+ blocked = frozenset(g_blocked)
+ return blocked
+ finally:
+ g_blockedlock.release()
+
+
+def set_grab(grabbing):
+ """This method will not be implemented"""
+
+
+def get_grab():
+ """This method will not be implemented"""
+
+
+def post(event):
+ """Post a new event to the Queue of events"""
+ g_blockedlock.acquire()
+ try:
+ if getattr(event, 'type', None) not in g_blocked:
+ g_events.put(event, block=False)
+ finally:
+ g_blockedlock.release()
+
+
+def makeseq(obj):
+ """Accept either a scalar object or a sequence, and return a sequence
+ over which we can iterate. If we were passed a sequence, return it
+ unchanged. If we were passed a scalar, return a tuple containing only
+ that scalar. This allows the caller to easily support one-or-many.
+ """
+ # Strings are the exception because you can iterate over their chars
+ # -- yet, for all the purposes I've ever cared about, I want to treat
+ # a string as a scalar.
+ if isinstance(obj, basestring):
+ return (obj)
+ try:
+ # Except as noted above, if you can get an iter() from an object,
+ # it's a collection.
+ iter(obj)
+ return obj
+ except TypeError:
+ # obj is a scalar. Wrap it in a tuple so we can iterate over the
+ # one item.
+ return (obj)
diff --git a/sugargame/mesh.py b/sugargame/mesh.py
new file mode 100644
index 0000000..58436aa
--- /dev/null
+++ b/sugargame/mesh.py
@@ -0,0 +1,435 @@
+import sugargame
+
+from sugar3.presence.tubeconn import TubeConnection
+from dbus.gobject_service import ExportedGObject
+from dbus.service import method, signal
+
+import telepathy
+import sugar3.presence.presenceservice
+import pygame.event as PEvent
+
+
+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_SERVICE = None
+
+
+### NEW PYGAME EVENTS ###
+
+CONNECT = sugargame.CONNECT
+PARTICIPANT_ADD = sugargame.PARTICIPANT_ADD
+PARTICIPANT_REMOVE = sugargame.PARTICIPANT_REMOVE
+MESSAGE_UNI = sugargame.MESSAGE_UNI
+MESSAGE_MULTI = sugargame.MESSAGE_MULTI
+
+
+# Private objects for useful purposes!
+pygametubes = []
+text_chan, tubes_chan = (None, None)
+conn = None
+initiating = False
+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
+ of when the Tube is ready.'''
+ global connect_callback
+ connect_callback = cb
+
+
+def activity_shared(activity):
+ '''Called when the user clicks Share.'''
+
+ global initiating
+ initiating = True
+
+ _setup(activity)
+
+ channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
+ if hasattr(channel, 'OfferDBusTube'):
+ channel.OfferDBusTube(DBUS_SERVICE, {})
+ else:
+ channel.OfferTube(telepathy.TUBE_TYPE_DBUS, DBUS_SERVICE, {})
+
+ global connect_callback
+ 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.'''
+
+ global initiating
+ global joining
+ initiating = False
+ joining = True
+
+ _setup(activity)
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=_list_tubes_reply_cb,
+ error_handler=_list_tubes_error_cb)
+
+ global connect_callback
+ if connect_callback is not None:
+ connect_callback()
+
+
+def _getConn(activity):
+ global conn
+ if conn:
+ return conn
+ else:
+ if hasattr(activity._shared_activity, 'telepathy_conn'):
+ conn = activity._shared_activity.telepathy_conn
+ else:
+ pservice = _get_presence_service()
+ name, path = pservice.get_preferred_connection()
+ conn = telepathy.client.Connection(name, path)
+ return conn
+
+
+def _setup(activity):
+ '''Determines text and tube channels for the current Activity. If no tube
+channel present, creates one. Updates text_chan and tubes_chan.
+
+setup(sugar.activity.Activity, telepathy.client.Connection)'''
+ global text_chan, tubes_chan, DBUS_SERVICE
+ if not DBUS_SERVICE:
+ DBUS_SERVICE = activity.get_bundle_id()
+ if not activity.get_shared():
+ raise "Failure"
+
+ if hasattr(activity._shared_activity, 'telepathy_tubes_chan'):
+ _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:
+ 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:
+ channel = telepathy.client.Channel(bus_name, channel_path)
+ htype, handle = channel.GetHandle()
+ if htype == telepathy.HANDLE_TYPE_ROOM:
+ room = handle
+ ctype = channel.GetChannelType()
+ if ctype == telepathy.CHANNEL_TYPE_TUBES:
+ tubes_chan = channel
+ elif ctype == telepathy.CHANNEL_TYPE_TEXT:
+ text_chan = channel
+
+ if room is None:
+ raise "Failure"
+
+ if text_chan is None:
+ raise "Failure"
+
+ # Make sure we have a Tubes channel - PS doesn't yet provide one
+ if tubes_chan is None:
+ tubes_chan = conn.request_channel(telepathy.CHANNEL_TYPE_TUBES,
+ telepathy.HANDLE_TYPE_ROOM, room, True)
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
+ new_tube_cb)
+
+ return (text_chan, tubes_chan)
+
+
+def new_tube_cb(id, initiator, type, service, params, state):
+ 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)
+ else:
+ channel.AcceptTube(id)
+
+ tube_conn = TubeConnection(conn,
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
+ id, group_iface=text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+
+ global pygametubes, initiating
+ pygametubes.append(PygameTube(tube_conn, initiating, len(pygametubes)))
+
+
+def _list_tubes_reply_cb(tubes):
+ for tube_info in tubes:
+ new_tube_cb(*tube_info)
+
+
+def _list_tubes_error_cb(e):
+ pass
+
+
+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
+ """
+
+ cs_handle = instance().tube.bus_name_to_handle[dbus_handle]
+ group = text_chan[telepathy.CHANNEL_INTERFACE_GROUP]
+ if not errback:
+ def errback(error):
+ pass
+
+ def with_my_csh(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)
+ elif group.GetGroupFlags() & \
+ telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ handle = group.GetHandleOwners([cs_handle])[0]
+ _withHandle(handle)
+ else:
+ handle = cs_handle
+ _withHandle(handle)
+ group.GetSelfHandle(reply_handler=with_my_csh, error_handler=errback)
+
+
+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():
+ """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...
+ """
+
+ try:
+ pservice = sugar3.presence.presenceservice.get_instance()
+ try:
+ name, path = pservice.get_preferred_connection()
+ except (TypeError, ValueError):
+ raise OfflineError("""Unable to retrieve buddy information,
+ currently offline""")
+ else:
+ return pservice
+ except Exception:
+ pass
+
+
+def instance(idx=0):
+ return pygametubes[idx]
+
+
+class PygameTube(ExportedGObject):
+ '''The object whose instance is shared across D-bus
+
+ Call instance() to get the instance of this object for
+ your activity service.
+ Its 'tube' property contains the underlying D-bus Connection.
+ '''
+ def __init__(self, tube, is_initiator, tube_id):
+ super(PygameTube, self).__init__(tube, DBUS_PATH)
+ self.tube = tube
+ self.is_initiator = is_initiator
+ self.entered = False
+ self.ordered_bus_names = []
+ 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):
+ for handle, bus_name in added:
+ dbus_handle = self.tube.participants[handle]
+ self.ordered_bus_names.append(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)
+ PEvent.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle))
+
+ if self.is_initiator:
+ if not self.entered:
+ # Initiator will broadcast a new ordered_bus_names each time
+ # a participant joins.
+ self.ordered_bus_names = [self.tube.get_unique_name()]
+ self.NewParticipants(self.ordered_bus_names)
+
+ self.entered = True
+
+ @signal(dbus_interface=DBUS_IFACE, signature='as')
+ def NewParticipants(self, ordered_bus_names):
+ '''This is the NewParticipants signal,
+ sent when the authoritative list of ordered_bus_names changes.'''
+ pass
+
+ @signal(dbus_interface=DBUS_IFACE, signature='s')
+ def Broadcast(self, content):
+ '''This is the Broadcast signal;
+ it sends a message to all other activity participants.'''
+ pass
+
+ @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.'''
+ 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.'''
+ 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.'''
+ if self.ordered_bus_names != new_bus_names:
+ self.ordered_bus_names = new_bus_names
+
+
+def send_to(handle, content=""):
+ '''Sends the given message to the given buddy identified by handle.'''
+ remote_proxy = dbus_get_object(handle, DBUS_PATH)
+ remote_proxy.Tell(content, reply_handler=dbus_msg, error_handler=dbus_err)
+
+
+def dbus_msg():
+ pass
+
+
+def dbus_err(e):
+ pass
+
+
+def broadcast(content=""):
+ '''Sends the given message to all participants.'''
+ instance().Broadcast(content)
+
+
+def my_handle():
+ '''Returns the handle of this user
+
+ Note, you can get a DBusException from this if you have
+ not yet got a unique ID assigned by the bus. You may need
+ to delay calling until you are sure you are connected.
+ '''
+ return instance().tube.get_unique_name()
+
+
+def is_initiator():
+ '''Returns the handle of this user.'''
+ 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.'''
+
+ try:
+ return instance().ordered_bus_names[:]
+ except IndexError:
+ 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
+
+ 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.
+ '''
+ 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. 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
+ '''
+ from sugargame 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/sugargame/mesh.pyc b/sugargame/mesh.pyc
new file mode 100644
index 0000000..104f1c6
--- /dev/null
+++ b/sugargame/mesh.pyc
Binary files differ
diff --git a/sugargame/svgsprite.py b/sugargame/svgsprite.py
new file mode 100644
index 0000000..e275433
--- /dev/null
+++ b/sugargame/svgsprite.py
@@ -0,0 +1,88 @@
+"""RSVG / Cairo-based rendering of SVG into Pygame Images"""
+from pygame import sprite, Rect
+from sugargame import _cairoimage
+
+
+class SVGSprite(sprite.Sprite):
+ """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__(
+ self, svg=None, size=None, *args
+ ):
+ """Initialise the svg sprite
+
+ svg -- svg source text (i.e. content of an svg file)
+ size -- optional, to constrain size, (width,height), leaving one
+ as None or 0 causes proportional scaling, leaving both
+ as None or 0 causes natural scaling (screen resolution)
+ args -- if present, groups to which to automatically add
+ """
+ self.size = size
+ super(SVGSprite, self).__init__(*args)
+ if svg:
+ self.setSVG(svg)
+
+ def setSVG(self, svg):
+ """Set our SVG source"""
+ self.svg = svg
+ # XXX could delay this until actually asked to display...
+ if self.size:
+ width, height = self.size
+ else:
+ width, height = None, None
+ 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...
+ self.rect = rect
+
+ def _render(self, width, height):
+ """Render our SVG to a Pygame image"""
+ import rsvg
+ handle = rsvg.Handle(data=self.svg)
+ scale = 1.0
+ hw, hh = handle.get_dimension_data()[:2]
+ if hw and hh:
+ if not width:
+ if not height:
+ width, height = hw, hh
+ else:
+ scale = float(height) / hh
+ width = hh / float(hw) * height
+ elif not height:
+ scale = float(width) / hw
+ height = hw / float(hh) * width
+ else:
+ # scale only, only rendering as large as it is...
+ if width / height > hw / hh:
+ # want it taller than it is...
+ width = hh / float(hw) * height
+ else:
+ height = hw / float(hh) * width
+ scale = float(height) / hh
+
+ csrf, ctx = _cairoimage.newContext(int(width), int(height))
+ ctx.scale(scale, scale)
+ 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/sugargame/util.py b/sugargame/util.py
new file mode 100644
index 0000000..ec2d3e5
--- /dev/null
+++ b/sugargame/util.py
@@ -0,0 +1,77 @@
+"""Abstraction layer for working outside the Sugar environment"""
+import traceback
+import cStringIO
+import os
+import os.path
+
+NON_SUGAR_ROOT = '~/.sugar/default/olpcgames'
+
+from sugar3.activity.activity import get_bundle_path as _get_bundle_path
+
+
+def get_bundle_path():
+ """Retrieve bundle path from activity with fix for silly registration bug"""
+ path = _get_bundle_path()
+ if path.endswith('.activity.activity'):
+ path = path[:-9]
+ return path
+
+
+def get_activity_root():
+ """Return the activity root for data storage operations
+
+ If the activity is present, returns the activity's root,
+ otherwise returns NON_SUGAR_ROOT as the directory.
+ """
+ import sugargame
+ if sugargame.ACTIVITY:
+ return sugargame.ACTIVITY.get_activity_root()
+ else:
+ return os.path.expanduser(NON_SUGAR_ROOT)
+
+
+def data_path(file_name):
+ """Return the full path to a file in the data sub-directory of the bundle"""
+ return os.path.join(get_bundle_path(), 'data', file_name)
+
+
+def tmp_path(file_name):
+ """Return the full path to a file in the temporary directory"""
+ return os.path.join(get_activity_root(), 'tmp', file_name)
+
+
+def get_traceback(error):
+ """Get formatted traceback from current exception
+
+ error -- Exception instance raised
+
+ Attempts to produce a 10-level traceback as a string
+ that you can log off. Use like so:
+
+ try:
+ doSomething()
+ except Exception, err:
+ log.error(
+ '''Failure during doSomething with X,Y,Z parameters: %s''',
+ util.get_traceback(err),
+ )
+ """
+ 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_path = cStringIO.StringIO()
+ try:
+ traceback.print_exc(limit=10, file=file_path)
+ exception = file.getvalue()
+ finally:
+ file.close()
+ return exception