diff options
author | Ignacio Rodriguez <ignacio@sugarlabs.org> | 2013-12-23 23:01:23 (GMT) |
---|---|---|
committer | Ignacio Rodriguez <ignacio@sugarlabs.org> | 2013-12-23 23:01:23 (GMT) |
commit | 0334288a7a2fa1292c9749fa9bb18aa75de910f1 (patch) | |
tree | aeb6b3a3950e7382e3cc5c6a13cda99728e3332a | |
parent | e0624bc74a623697860cefaa04cc440cbc24bcdc (diff) |
port to gtk3 and sugargame a lot of bugs.
-rwxr-xr-x | activity.py | 70 | ||||
-rw-r--r-- | game.py | 81 | ||||
-rw-r--r-- | player.py | 2 | ||||
-rw-r--r-- | sugargame/__init__.py | 7 | ||||
-rw-r--r-- | sugargame/data/__init__.py | 35 | ||||
-rw-r--r-- | sugargame/data/sleeping_svg.py | 61 | ||||
-rw-r--r-- | sugargame/dbusproxy.py | 89 | ||||
-rw-r--r-- | sugargame/eventwrap.py | 417 | ||||
-rw-r--r-- | sugargame/mesh.py | 435 | ||||
-rw-r--r-- | sugargame/mesh.pyc | bin | 0 -> 15664 bytes | |||
-rw-r--r-- | sugargame/svgsprite.py | 88 | ||||
-rw-r--r-- | sugargame/util.py | 77 |
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')) @@ -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) @@ -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 Binary files differnew file mode 100644 index 0000000..104f1c6 --- /dev/null +++ b/sugargame/mesh.pyc 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 |