From b13376f98578c5bc16e4c1a395940827258b226e Mon Sep 17 00:00:00 2001 From: Jorge Levera Date: Fri, 28 May 2010 06:02:14 +0000 Subject: Changed module olpcgame for the one that comes with Maze Activity. Added logging and sound card detection to enable multiplayer on same machine. Added toolbar and allows joining activity, however, it is still buggy on multiplayer mode --- diff --git a/conozcopy.py b/conozcopy.py index 9d69e58..13f08ca 100755 --- a/conozcopy.py +++ b/conozcopy.py @@ -30,6 +30,30 @@ import os import pygame import olpcgames import gtk +import gobject +from gettext import gettext as _ + +from sugar import logger +import logging +import logging.config + +_logger = logging.getLogger('conozco-paraguay') +_logger.setLevel(logging.DEBUG) +_SOUND = True +_NEW_TOOLBAR_SUPPORT = True +try: + from sugar.graphics.toolbutton import ToolButton + from sugar.graphics.toolbarbox import ToolbarBox + from sugar.graphics.toolbarbox import ToolbarButton + from sugar.activity.widgets import StopButton + from toolbar import ViewToolbar + from mybutton import MyActivityToolbarButton + _logger.debug('NEW toolbar') +except: + _logger.debug('old toolbar') + _NEW_TOOLBAR_SUPPORT = False + + # constantes RADIO = 10 @@ -90,6 +114,30 @@ def wait_events(): clock.tick(20) return pygame.event.get() +#class ViewToolbar(gtk.Toolbar): +# __gtype_name__ = 'ViewToolbar' + +# __gsignals__ = { +# 'needs-update-size': (gobject.SIGNAL_RUN_FIRST, +# gobject.TYPE_NONE, +# ([])), +# 'go-fullscreen': (gobject.SIGNAL_RUN_FIRST, +# gobject.TYPE_NONE, +# ([])) +# } + +# def __init__(self): +# gtk.Toolbar.__init__(self) +# self.fullscreen = ToolButton('view-fullscreen') +# self.fullscreen.set_tooltip(_('Fullscreen')) +# self.fullscreen.connect('clicked', self.fullscreen_cb) +# self.insert(self.fullscreen, -1) +# self.fullscreen.show() + +# def fullscreen_cb(self, button): +# self.emit('go-fullscreen') + + class Punto(): """Clase para objetos geograficos que se pueden definir como un punto. @@ -487,7 +535,7 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true self.pantalla.blit(self.pantallaTemp,(0,0)) pygame.display.flip() return @@ -566,11 +614,11 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN: if event.key == 27: # escape: volver - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true self.elegir_directorio = True return elif event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pos = event.pos if pos[1] > 275*scale + shift_y: # zona de opciones if pos[0] < 600*scale + shift_x: # primera columna @@ -712,7 +760,7 @@ class ConozcoPy(): self.click.play() sys.exit() elif event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pos = event.pos if pos[1] > 175*scale+shift_y: # zona de opciones if pos[0] < 600*scale+shift_x: # primera columna @@ -784,9 +832,32 @@ class ConozcoPy(): del imagen0 return imagen +# copiado desde http://en.flossmanuals.net/ActivitiesGuideSugar/ActivitiesUsingPyGame +# def build_toolbar(self): +# toolbox = activity.ActivityToolbox(olpcgames.ACTIVITY) +# activity_toolbar = toolbox.get_activity_toolbar() +# activity_toolbar.keep.props.visible = True +# activity_toolbar.share.props.visible = True +# +# olpcgames.ACTIVITY.view_toolbar = ViewToolbar() +# toolbox.add_toolbar(_('View'), olpcgames.ACTIVITY.view_toolbar) +# #olpcgames.ACTIVITY.view_toolbar.connect('go-fullscreen',olpcgames.ACTIVITY.view_toolbar_go_fullscreen_cb) +# olpcgames.ACTIVITY.view_toolbar.show() +# +# toolbox.show() +# olpcgames.ACTIVITY.set_toolbox(toolbox) + + # def view_toolbar_go_fullscreen_cb(self, view_toolbar): +# self.fullscreen() + + + def __init__(self): """Esta es la inicializacion del juego""" global scale, shift_x, shift_y, xo_resolution + _logger.debug('starting activity') + + pygame.init() # crear pantalla self.anchoPantalla = gtk.gdk.screen_width() @@ -794,6 +865,8 @@ class ConozcoPy(): self.altoPantalla = gtk.gdk.screen_height() - TOOLBAR_HEIGHT self.pantalla = pygame.display.set_mode((self.anchoPantalla, self.altoPantalla)) + _logger.debug('altoPantalla'+ str(self.altoPantalla)) + _logger.debug('anchoPantalla'+ str(self.anchoPantalla)) if self.anchoPantalla==1200 and self.altoPantalla==900: xo_resolution = True scale = 1 @@ -842,13 +915,20 @@ class ConozcoPy(): self.camino_sonidos = os.path.join(CAMINORECURSOS, CAMINOCOMUN, CAMINOSONIDOS) - self.despegue = pygame.mixer.Sound(os.path.join(\ - self.camino_sonidos,"NoiseCollector_boom2.ogg")) - self.click = pygame.mixer.Sound(os.path.join(\ - self.camino_sonidos,"junggle_btn045.wav")) - self.click.set_volume(0.2) - self.chirp = pygame.mixer.Sound(os.path.join(\ - self.camino_sonidos,"chirp_alerta.ogg")) + #if sound is not available, then no sound + try: + self.despegue = pygame.mixer.Sound(os.path.join(\ + self.camino_sonidos,"NoiseCollector_boom2.ogg")) + self.click = pygame.mixer.Sound(os.path.join(\ + self.camino_sonidos,"junggle_btn045.wav")) + self.click.set_volume(0.2) + self.chirp = pygame.mixer.Sound(os.path.join(\ + self.camino_sonidos,"chirp_alerta.ogg")) + except: + _SOUND = False + _logger.debug("exploto: " + str(sys.exc_info()[0])+" "+str(sys.exc_info()[1])+" "+str(sys.exc_info()[2])) + _logger.debug("NO SOUND") + # cargar directorios self.cargarListaDirectorios() # cargar fuentes @@ -936,11 +1016,17 @@ class ConozcoPy(): self.cursor_espera = pygame.cursors.compile(datos_cursor_espera) try: + pass + """_logger.debug('try 1') toolbox = activity.ActivityToolbox(olpcgames.ACTIVITY) + _logger.debug('try 2') olpcgames.ACTIVITY.set_toolbox(toolbox) + _logger.debug('try 3') toolbox.show() + _logger.debug('try 4') """ + #self.build_toolbar() except: - pass + _logger.debug("exploto: " + str(sys.exc_info()[0])+" "+str(sys.exc_info()[1])+" "+str(sys.exc_info()[2])) def cargarDirectorio(self): """Carga la informacion especifica de un directorio""" @@ -1151,10 +1237,11 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN: if event.key == 27: # escape: salir - self.click.play() + if _SOUND: + self.click.play() return elif event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true if event.pos[0] < XMAPAMAX*scale+shift_x: # zona de mapa for i in self.nivelActual.elementosActivos: if i.startswith("capitales"): @@ -1293,12 +1380,13 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN: if event.key == 27: # escape: salir - self.click.play() + if _SOUND: + self.click.play() pygame.time.set_timer(EVENTORESPUESTA,0) pygame.time.set_timer(EVENTODESPEGUE,0) return elif event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true if self.avanceNivel < TOTALAVANCE: if event.pos[0] < XMAPAMAX*scale+shift_x: # zona mapa self.borrarGlobito() @@ -1351,7 +1439,8 @@ class ConozcoPy(): (int(XMAPAMAX*scale+shift_x),0, int(DXPANEL*scale), int(900*scale))) - self.despegue.play() + if _SOUND: + self.despegue.play() self.pantalla.fill(COLORPANEL, (int(XNAVE*scale+shift_x), self.yNave, @@ -1404,14 +1493,15 @@ class ConozcoPy(): (255,155,155)) pygame.display.flip() pygame.time.set_timer(EVENTODESPEGUE,TIEMPODESPEGUE) - self.despegue.play() + if _SOUND: + self.despegue.play() self.paso = 0 terminar = False while 1: for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTODESPEGUE,0) return elif event.type == EVENTODESPEGUE: @@ -1462,7 +1552,7 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTORESPUESTA,0) return elif event.type == EVENTORESPUESTA: @@ -1483,7 +1573,7 @@ class ConozcoPy(): (int(600*scale+shift_x),int(800*scale+shift_y)), (255,155,155)) pygame.display.flip() - self.chirp.play() + (lambda : _SOUND and self.chirp.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTORESPUESTA,500) self.paso = 0 terminar = False @@ -1491,7 +1581,7 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTORESPUESTA,0) return elif event.type == EVENTORESPUESTA: @@ -1508,7 +1598,7 @@ class ConozcoPy(): self.pantalla.blit(self.alertarojo, (int(459*scale+shift_x), int(297*scale+shift_y))) - self.chirp.play() + (lambda : _SOUND and self.chirp.play() or _SOUND)() #ejecuta el play si sound es true else: self.pantalla.blit(self.alerta, (int(264*scale+shift_x), @@ -1543,7 +1633,7 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTORESPUESTA,0) return elif event.type == EVENTORESPUESTA: @@ -1553,7 +1643,7 @@ class ConozcoPy(): pygame.display.flip() if terminar: break - # cuadro 5: explota nave + # cuadro 5: explota nave_ self.pantalla.blit(self.tierra,(int(200*scale+shift_x), int(150*scale+shift_y))) self.mostrarTexto("Presiona cualquier tecla para saltear", @@ -1562,14 +1652,15 @@ class ConozcoPy(): (255,155,155)) pygame.display.flip() pygame.time.set_timer(EVENTODESPEGUE,TIEMPODESPEGUE) - self.despegue.play() + if _SOUND: + self.despegue.play() self.paso = 0 terminar = False while 1: for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTODESPEGUE,0) return elif event.type == EVENTODESPEGUE: @@ -1640,7 +1731,7 @@ class ConozcoPy(): for event in wait_events(): if event.type == pygame.KEYDOWN or \ event.type == pygame.MOUSEBUTTONDOWN: - self.click.play() + (lambda : _SOUND and self.click.play() or _SOUND)() #ejecuta el play si sound es true pygame.time.set_timer(EVENTORESPUESTA,0) return elif event.type == EVENTORESPUESTA: diff --git a/olpcgames/__init__.py b/olpcgames/__init__.py index 504388c..5fd28b7 100755..100644 --- a/olpcgames/__init__.py +++ b/olpcgames/__init__.py @@ -1,6 +1,6 @@ -"""Wrapper/adaptation system for writing/porting Pygame games to OLPC/Sugar +"""Wrapper/adaptation system for writing/porting PyGame games to OLPC/Sugar -The wrapper system attempts to substitute various pieces of the Pygame +The wrapper system attempts to substitute various pieces of the PyGame implementation in order to make code written without knowledge of the OLPC/Sugar environment run "naturally" under the GTK environment of Sugar. It also provides some convenience mechanisms for dealing with @@ -8,9 +8,9 @@ e.g. the Camera and Mesh Network system. Considerations for Developers: -Pygame programs running under OLPCGames will generally not have +PyGame programs running under OLPCGames will generally not have "hardware" surfaces, and will not be able to have a reduced-resolution -full-screen view to optimise rendering. The Pygame code will run in +full-screen view to optimise rendering. The PyGame code will run in a secondary thread, with the main GTK UI running in the primary thread. A third "mainloop" thread will occasionally be created to handle the GStreamer interface to the camera. @@ -22,81 +22,21 @@ Attributes of Note: WIDGET -- PygameCanvas instance, a GTK widget with an embedded socket object which is a proxy for the SDL window Pygame to which pygame renders. - - Constants: All event constants used by the package are defined at this - level. Note that eventually we will need to switch to using UserEvent - and making these values sub-types rather than top-level types. - - -Pygame events at the Activity Level: - - pygame.USEREVENT - code == olpcgames.FILE_READ_REQUEST - filename (unicode/string) -- filename from which to read - metadata (dictionary-like) -- mapping from key to string values - - Note: due to a limitation in the Sugar API, the GTK event loop - will be *frozen* during this operation, as a result you cannot - make any DBUS or GTK calls, nor can you use GUI during the - call to provide input. That is, you have to process this event - synchronously. - - code == olpcgames.FILE_WRITE_REQUEST - filename (unicode/string) -- file name to which to write - metadata (dictionary-like) -- mapping from key: value where all - values must (currently) be strings - - Note: due to a limitation in the Sugar API, the GTK event loop - will be *frozen* during this operation, as a result you cannot - make any DBUS or GTK calls, nor can you use GUI during the - call to provide input. That is, you have to process this event - synchronously. - -see also the mesh and camera modules for more events. - -Deprecated: - - This module includes the activity.PyGameActivity class currently, - this is a deprecated mechanism for accessing the activity class, - and uses the deprecated spelling (case) of the name. Use: - - from olpcgames import activity - - class MyActivity( activity.PygameActivity ): - ... - - to define your PygameActivity subclass (note the case of the - spelling, which now matches Pygame's own spelling). """ -from olpcgames._version import __version__ -ACTIVITY = None -widget = WIDGET = None - -# XXX problem here, we're filling up the entirety of the Pygame -# event-set with just this small bit of functionality, obviously -# Pygame is not intending for this kind of usage! -( - CAMERA_LOAD, CAMERA_LOAD_FAIL, - - CONNECT,PARTICIPANT_ADD, - PARTICIPANT_REMOVE, - MESSAGE_UNI,MESSAGE_MULTI, -) = range( 25, 32 ) - -# These events use UserEvent.code, eventually *all* events should be -# delivered as UserEvent with code set to the values defined here... - -( - #NET_CONNECT, NET_PARTICIPANT_ADD,NET_PARTICIPANT_REMOVE, - #NET_MESSAGE_UNICAST, NET_MESSAGE_MULTICAST, - #CAMERA_LOAD, CAMERA_LOAD_FAIL, - FILE_READ_REQUEST, FILE_WRITE_REQUEST, -) = range( - 2**16, 2**16+2, -) - +# XXX handle configurations that are not running under Sugar and +# report proper errors to describe the problem, rather than letting the +# particular errors propagate outward. +# XXX allow use of a particular feature within the library without needing +# to be running under sugar. e.g. allow importing mesh or camera without +# needing to import the activity machinery. +from olpcgames.canvas import * try: - from olpcgames.activity import PygameActivity as PyGameActivity + from olpcgames.activity import * except ImportError, err: - PyGameActivity = None + PyGameActivity = None +from olpcgames import camera +from olpcgames import pangofont +from olpcgames import mesh +ACTIVITY = None +widget = WIDGET = None diff --git a/olpcgames/_cairoimage.py b/olpcgames/_cairoimage.py index 3cfa22c..302b916 100755..100644 --- a/olpcgames/_cairoimage.py +++ b/olpcgames/_cairoimage.py @@ -1,38 +1,23 @@ -"""Utility functions for cairo-specific operations - -USE_BASE_ARRAY -- if False (default), uses numpy arrays, - currently this is the only version that works on 32-bit - machines. -""" -import pygame, struct, logging +"""Utility functions for cairo-specific operations""" +import cairo, pygame, struct big_endian = struct.pack( '=i', 1 ) == struct.pack( '>i', 1 ) -log = logging.getLogger( 'olpcgames._cairoimage' ) -##log.setLevel( logging.DEBUG ) - -USE_BASE_ARRAY = False - def newContext( width, height ): """Create a new render-to-image context width, height -- pixel dimensions to be rendered - Produces an ARGB format Cairo ImageSurface for - rendering your data into using rsvg, Cairo or Pango. - - returns (ImageSurface, CairoContext) for rendering + returns surface, context for rendering """ - import cairo csrf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - context = cairo.Context (csrf) - #log.info( 'Format (expect: %s): %s', cairo.FORMAT_ARGB32, csrf.get_format()) - return csrf, context + return csrf, cairo.Context (csrf) def mangle_color(color): """Mange a colour depending on endian-ness, and swap-necessity - Converts a 3 or 4 int (or float) value in the range 0-255 into a - 4-float value in the range 0.0-1.0 + This implementation has only been tested on an AMD64 + machine with a get_data implementation (rather than + a get_data_as_rgba implementation). """ r,g,b = color[:3] if len(color) > 3: @@ -46,58 +31,23 @@ def _fixColorBase( v ): return max((0,min((v,255.0))))/255.0 def asImage( csrf ): - """Get the pixels in csrf as a Pygame image - - Note that Pygame 1.7.1 on (Gentoo Linux) AMD64 is incorrectly - calculating the required size ARGB images, so this code will *not* work - on that platform with that version of the library. Pygame-ctypes - does work correctly there. - - Note also that Pygame 1.7.1 is showing a strange colour rotation - bug on 32-bit platforms, such that ARGB mode cannot be used for - images there. Instead we have to do an expensive bit-shift operation - to produce an RGBA image from the ARGB native Cairo format. - - Will raise a ValueError if passed a Null image (i.e. dimension of 0) - - returns Pygame.Surface (image) with convert_alpha() called for it. - """ + """Get the pixels in csrf as a Pygame image""" # Create and return a new Pygame Image derived from the Cairo Surface format = 'ARGB' if hasattr(csrf,'get_data'): # more recent API, native-format, but have to (potentially) convert the format... - log.debug( 'Native-mode api (get_data)' ) data = csrf.get_data() if not big_endian: - # we use array here because it's considerably lighter-weight - # to import than the numpy module - log.debug( 'Not big-endian, byte-swapping array' ) - if USE_BASE_ARRAY: - import array - a = array.array( 'I' ) - a.fromstring( data ) - a.byteswap() - data = a.tostring() - else: - import numpy - n = numpy.fromstring( data, dtype='I' ) - n = ((n & 0xff000000) >> 24 ) | ((n & 0x00ffffff) << 8 ) - n = n.byteswap() - data = n.tostring() - format = 'RGBA' + import numpy + data = numpy.frombuffer( data, 'I' ).astype( '>I4' ).tostring() else: - log.debug( 'Big-endian, array unchanged' ) data = str(data) # there's one copy else: # older api, not native, but we know what it is... - log.debug( 'Non-native mode api, explicitly RGBA' ) data = csrf.get_data_as_rgba() data = str(data) # there's one copy - format = 'RGBA' width, height = csrf.get_width(),csrf.get_height() - try: - log.info( 'Format = %s', format ) return pygame.image.fromstring( data, (width,height), @@ -106,30 +56,3 @@ def asImage( csrf ): except ValueError, err: err.args += (len(data), (width,height), width*height*4,format ) raise - -if __name__ == "__main__": - import unittest - logging.basicConfig() - class Tests( unittest.TestCase ): - def test_colours( self ): - """Test that colours are correctly translated - - If we draw a given colour in cairo, we want the same - colour to show up in Pygame, let's test that... - """ - for sourceColour in [ - (255,0,0, 255), - (0,255,0, 255), - (0,0,255, 255), - (255,255,0, 255), - (0,255,255,255), - (255,0,255,255), - ]: - csrf,cctx = newContext( 1,1 ) - background = mangle_color( sourceColour ) - cctx.set_source_rgba(*background) - cctx.paint() - img = asImage( csrf ) - colour = img.get_at( (0,0)) - assert colour == sourceColour, (sourceColour,mangle_color(sourceColour),colour) - unittest.main() diff --git a/olpcgames/_gtkmain.py b/olpcgames/_gtkmain.py deleted file mode 100755 index 33a6a83..0000000 --- a/olpcgames/_gtkmain.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Support for GObject mainloop-requiring libraries when not inside GTK - -INITIALIZED -- whether we have a running gobject loop yet... -LOOP_TRACKER -- if present, the manual gtk event loop used to - support gobject-based code running in a non-Gobject event loop - -Holder -- objects which can be held as attributes to keep the mainloop running -""" -import threading, logging -log = logging.getLogger( 'olpcgames._gtkmain' ) -##log.setLevel( logging.DEBUG ) - -INITIALIZED = False -LOOP_TRACKER = None - -class _TrackLoop( object ): - """Tracks the number of open loops and stops when finished""" - count = 0 - _mainloop = None - def increment( self ): - log.info( 'Increment from %s', self.count ) - self.count += 1 # XXX race condition here? - if self.count == 1: - log.info( 'Creating GObject mainloop') - self.t_loop = threading.Thread(target=self.loop) - self.t_loop.setDaemon( True ) - self.t_loop.start() - def decrement( self ): - log.info( 'Decrement from %s', self.count ) - self.count -= 1 - def loop( self ): - """Little thread loop that replicates the gtk mainloop""" - import gtk - while self.count >= 1: - log.debug( 'GTK loop restarting' ) - while gtk.events_pending(): - gtk.main_iteration() - log.debug( 'GTK loop exiting' ) - try: - del self.t_loop - except AttributeError, err: - pass - -class Holder(): - """Object which, while held, keeps the gtk mainloop running""" - def __init__( self ): - log.info( 'Beginning hold on GTK mainloop with Holder object' ) - startGTK() - def __del__( self ): - log.info( 'Releasing hold on GTK mainloop with Holder object' ) - stopGTK() - -def startGTK( ): - """GTK support is required here, process...""" - if not INITIALIZED: - init() - if LOOP_TRACKER: - LOOP_TRACKER.increment() -def stopGTK( ): - """GTK support is no longer required, release""" - if LOOP_TRACKER: - LOOP_TRACKER.decrement() -def init( ): - """Create a gobject mainloop in a sub-thread (you don't need to call this normally)""" - global INITIALIZED, LOOP_TRACKER - if not INITIALIZED: - if not LOOP_TRACKER: - LOOP_TRACKER = _TrackLoop() - INITIALIZED = True - return LOOP_TRACKER diff --git a/olpcgames/_version.py b/olpcgames/_version.py deleted file mode 100755 index 6a4e1db..0000000 --- a/olpcgames/_version.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Module defining the current version of the library""" -__version__ = '1.6' diff --git a/olpcgames/activity.py b/olpcgames/activity.py index d4a2b5a..45a6a69 100755..100644 --- a/olpcgames/activity.py +++ b/olpcgames/activity.py @@ -1,73 +1,48 @@ -"""Embeds the Canvas widget into a Sugar-specific Activity environment - -The olpcgames.activity module encapsulates creation of a Pygame activity. -Your Activity should inherit from this class. Simply setting some class -attributes is all you need to do in a class inheriting from -olpcgames.activity.PygameActivity in order to get Pygame to work. - -(The skeleton builder script creates this file automatically for you). - -Note: - You should not import pygame into your activity file, as the olpcgames - wrapper needs to be initialized before pygame is imported the first time. - -Example usage: - - class PygameActivity(activity.Activity): - game_name = None - game_title = 'Pygame Game' - game_size = (units.grid_to_pixels(16), - units.grid_to_pixels(11)) - pygame_mode = 'SDL' -""" +"""Embeds the Canvas widget into a Sugar-specific Activity environment""" import logging logging.root.setLevel( logging.WARN ) log = logging.getLogger( 'olpcgames.activity' ) -##log.setLevel( logging.DEBUG ) +#log.setLevel( logging.INFO ) import pygtk pygtk.require('2.0') import gtk import gtk.gdk -import os from sugar.activity import activity from sugar.graphics import style -from olpcgames.canvas import PygameCanvas +from olpcgames.canvas import PyGameCanvas from olpcgames import mesh, util -__all__ = ['PygameActivity'] +__all__ = ['PyGameActivity'] -class PygameActivity(activity.Activity): - """Pygame-specific activity type, provides boilerplate toolbar, creates canvas +class PyGameActivity(activity.Activity): + """PyGame-specific activity type, provides boilerplate toolbar, creates canvas Subclass Overrides: game_name -- specifies a fully-qualified name for the game's main-loop format like so: 'package.module:main' - if no function name is provided, "main" is assumed. - - game_handler -- DEPRECATED. alternate specification via direct - reference to a main-loop function. + if not function name is provided, "main" is assumed. + game_handler -- alternate specification via direct reference to a main-loop + function game_size -- two-value tuple specifying the size of the display in pixels, this is currently static, so once the window is created it cannot be changed. - If None, use the bulk of the screen for the Pygame surface based on + If None, use the bulk of the screen for the PyGame surface based on the values reported by the gtk.gdk functions. Note that None is *not* the default value. game_title -- title to be displayed in the Sugar Shell UI pygame_mode -- chooses the rendering engine used for handling the - Pygame drawing mode, 'SDL' chooses the standard Pygame renderer, + PyGame drawing mode, 'SDL' chooses the standard PyGame renderer, 'Cairo' chooses the experimental pygamecairo renderer. - - Note: You likely do *not* want to use Cairo, it is no longer maintained. - PYGAME_CANVAS_CLASS -- normally PygameCanvas, but can be overridden + PYGAME_CANVAS_CLASS -- normally PyGameCanvas, but can be overridden if you want to provide a different canvas class, e.g. to provide a different internal layout. Note: only used where pygame_mode == 'SDL' @@ -85,7 +60,7 @@ class PygameActivity(activity.Activity): method. If so, please report them to Mike Fletcher. """ game_name = None - game_title = 'Pygame Game' + game_title = 'PyGame Game' game_handler = None game_size = (16 * style.GRID_CELL_SIZE, 11 * style.GRID_CELL_SIZE) @@ -93,7 +68,7 @@ class PygameActivity(activity.Activity): def __init__(self, handle): """Initialise the Activity with the activity-description handle""" - super(PygameActivity, self).__init__(handle) + super(PyGameActivity, self).__init__(handle) self.make_global() if self.game_size is None: width,height = gtk.gdk.screen_width(), gtk.gdk.screen_height() @@ -101,10 +76,9 @@ class PygameActivity(activity.Activity): # for now just fudge the toolbar size... self.game_size = width, height - (1*style.GRID_CELL_SIZE) self.set_title(self.game_title) -# toolbar = self.build_toolbar() -# log.debug( 'Toolbar size: %s', toolbar.get_size_request()) + toolbar = self.build_toolbar() + log.debug( 'Toolbar size: %s', toolbar.get_size_request()) canvas = self.build_canvas() - self.connect( 'configure-event', canvas._translator.do_resize_event ) def make_global( self ): """Hack to make olpcgames.ACTIVITY point to us @@ -154,9 +128,9 @@ class PygameActivity(activity.Activity): toolbar.title.unset_flags(gtk.CAN_FOCUS) return toolbar - PYGAME_CANVAS_CLASS = PygameCanvas + PYGAME_CANVAS_CLASS = PyGameCanvas def build_canvas( self ): - """Construct the Pygame or PygameCairo canvas for drawing""" + """Construct the PyGame or PyGameCairo canvas for drawing""" assert self.game_handler or self.game_name, 'You must specify a game_handler or game_name on your Activity (%r)'%( self.game_handler or self.game_name ) @@ -165,8 +139,6 @@ class PygameActivity(activity.Activity): self.set_canvas(self._pgc) self._pgc.grab_focus() self._pgc.connect_game(self.game_handler or self.game_name) - # XXX Bad coder, do not hide in a widely subclassed operation - # map signal does not appear to show up on socket instances gtk.gdk.threads_init() return self._pgc else: @@ -188,54 +160,3 @@ class PygameActivity(activity.Activity): mod = __import__(mod_name, globals(), locals(), []) fn = getattr(mod, fn_name) fn() - def read_file(self, file_path): - """Handle request to read the given file on the Pygame side - - This is complicated rather noticeably by the silly semantics of the Journal - where it unlinks the file as soon as this method returns. We either have to - handle the file-opening in PyGTK (not acceptable), block this thread until - the Pygame thread handles the event (which it may never do) or we have - to make the silly thing use a non-standard file-opening interface. - """ - log.info( 'read_file: %s %s', file_path, self.metadata ) - import olpcgames, pygame - from olpcgames import eventwrap - event = eventwrap.Event( - type = pygame.USEREVENT, - code = olpcgames.FILE_READ_REQUEST, - filename = file_path, - metadata = self.metadata, - ) - eventwrap.post( event ) - event.block() - def write_file( self, file_path ): - """Handle request to write to the given file on the Pygame side - - This is rather complicated by the need to have the file complete by the - time the function returns. Very poor API, after all, if I have to write a - multi-hundred-megabyte file it might take many minutes to complete - writing. - """ - log.info( 'write_file: %s %s', file_path, self.metadata ) - if os.path.exists( file_path ): - self.read_file( file_path ) - import olpcgames, pygame - from olpcgames import eventwrap - event = eventwrap.Event( - type = pygame.USEREVENT, - code = olpcgames.FILE_WRITE_REQUEST, - filename = file_path, - metadata = self.metadata, - ) - eventwrap.post( event ) - event.block() - if not os.path.exists( file_path ): - log.warn( '''No file created in %r''', file_path ) - raise NotImplementedError( """Pygame Activity code did not produce a file for %s"""%( file_path, )) - else: - log.info( '''Stored file in %r''', file_path ) - - -import olpcgames -olpcgames.PyGameActivity = PygameActivity -PyGameActivity = PygameActivity diff --git a/olpcgames/buildmanifest.py b/olpcgames/buildmanifest.py deleted file mode 100755 index 899433b..0000000 --- a/olpcgames/buildmanifest.py +++ /dev/null @@ -1,33 +0,0 @@ -#! /usr/bin/env python -"""Stupid little script to automate generation of MANIFEST and po/POTFILES.in - -Really this should have been handled by using distutils, but oh well, -distutils is a hoary beast and I can't fault people for not wanting to -spend days spelunking around inside it to find the solutions... -""" -from distutils.filelist import FileList -import os - -def fileList( template ): - """Produce a formatted file-list for storing in a file""" - files = FileList() - for line in filter(None,template.splitlines()): - files.process_template_line( line ) - content = '\n'.join( files.files ) - return content - - -def main( ): - """Do the quicky finding of files for our manifests""" - content = fileList( open('MANIFEST.in').read() ) - open( 'MANIFEST','w').write( content ) - - content = fileList( open('POTFILES.in').read() ) - try: - os.makedirs( 'po' ) - except OSError, err: - pass - open( os.path.join('po','POTFILES.in'), 'w').write( content ) - -if __name__ == "__main__": - main() diff --git a/olpcgames/camera.py b/olpcgames/camera.py index 249f295..b51a394 100755..100644 --- a/olpcgames/camera.py +++ b/olpcgames/camera.py @@ -2,27 +2,38 @@ Depends upon: pygame - gstreamer (particularly gst-launch) - -Activity demonstrating usage: - - http://dev.laptop.org/git?p=projects/games-misc;a=tree;f=cameratest.activity;hb=HEAD - - + python-gstreamer """ -import threading, subprocess +import threading import logging -import olpcgames import time import os import pygame +import gst from olpcgames.util import get_activity_root log = logging.getLogger( 'olpcgames.camera' ) #log.setLevel( logging.DEBUG ) -CAMERA_LOAD, CAMERA_LOAD_FAIL = olpcgames.CAMERA_LOAD, olpcgames.CAMERA_LOAD +CAMERA_LOAD = 9917 +CAMERA_LOAD_FAIL = 9918 +class CameraSprite(object): + """Create gstreamer surface for the camera.""" + def __init__(self, x, y): + import olpcgames + if olpcgames.WIDGET: + self._init_video(olpcgames.WIDGET, x, y) + + def _init_video(self, widget, x, y): + from olpcgames import video + self._vid = video.VideoWidget() + widget._fixed.put(self._vid, x, y) + self._vid.show() + + self.player = video.Player(self._vid) + self.player.play() + class Camera(object): """A class representing a still-picture camera @@ -40,18 +51,11 @@ class Camera(object): Note: The Camera class is simply a convenience wrapper around a fairly - straightforward gst-launch bus. If you have more involved + straightforward gstreamer bus. If you have more involved requirements for your camera manipulations you will probably find it easier to write your own camera implementation than to use this one. Basically we provide here the "normal" use case of snapping a picture into a pygame image. - - Note: - - With the current camera implementation taking a single photograph - requires about 6 seconds! Obviously we'll need to figure out what's - taking gstreamer so long to process the pipe and fix that. - """ _aliases = { 'camera': 'v4l2src', @@ -61,7 +65,7 @@ class Camera(object): 'jpeg': 'jpegenc', 'jpg': 'jpegenc', } - def __init__(self, source='camera', format='png', filename=None, directory = None): + def __init__(self, source='camera', format='png', filename='snap.png', directory = None): """Initialises the Camera's internal description source -- the gstreamer source for the video to capture, useful values: @@ -70,22 +74,17 @@ class Camera(object): format -- the gstreamer encoder to use for the capture, useful values: 'pngenc','png' -- PNG format graphic 'jpegenc','jpg','jpeg' -- JPEG format graphic - filename -- the filename to use for the capture, if not specified defaults - to a random UUID + '.' + format + filename -- the filename to use for the capture directory -- the directory in which to create the temporary file, defaults to get_activity_root() + 'tmp' """ - log.info( 'Creating camera' ) - if not filename: - import uuid - filename = '%s.%s'%( uuid.uuid4(), format ) self.source = self._aliases.get( source, source ) self.format = self._aliases.get( format, format ) self.filename = filename self.directory = directory - SNAP_PIPELINE = 'gst-launch','%(source)s','!','ffmpegcolorspace','!','%(format)s','!','filesink','location="%(filename)s"' - def _create_subprocess( self ): - """Method to create the gstreamer subprocess from our settings""" + SNAP_PIPELINE = '%(source)s ! ffmpegcolorspace ! %(format)s ! filesink location="%(filename)s"' + def _create_pipe( self ): + """Method to create the cstreamer pipe from our settings""" if not self.directory: path = os.path.join( get_activity_root(), 'tmp' ) try: @@ -98,27 +97,35 @@ class Camera(object): filename = os.path.join( path, self.filename ) format = self.format source = self.source - pipeline = [s%locals() for s in self.SNAP_PIPELINE ] - return filename, subprocess.Popen( - pipeline,stderr = subprocess.PIPE - ) - + pipeline = self.SNAP_PIPELINE % locals() + log.debug( 'Background thread processing: %s', pipeline ) + return filename, gst.parse_launch(pipeline) + def snap(self): """Snap a picture via the camera by iterating gstreamer until finished Note: this is an unsafe implementation, it will cause the whole - activity to hang until the capture finishes. Time to finish is often - measured in whole seconds (3-6s). - - It is *strongly* recommended that you use snap_async instead of snap! + activity to hang if the operation happens to fail! It is strongly + recommended that you use snap_async instead of snap! """ log.debug( 'Starting snap' ) - filename, pipe = self._create_subprocess() - if not pipe.wait(): - log.debug( 'Ending snap, loading: %s', filename ) - return self._load_and_clean( filename ) - else: - raise IOError( """Unable to complete snapshot: %s""", pipe.stderr.read() ) + filename, pipe = self._create_pipe() + pipe.set_state(gst.STATE_PLAYING) + bus = pipe.get_bus() + tmp = False + while True: + event = self.bus.poll(gst.MESSAGE_STATE_CHANGED, 5) + if event: + old, new, pending = event.parse_state_changed() + if pending == gst.STATE_VOID_PENDING: + if tmp: + break + else: + tmp = True + else: + break + log.log( 'Ending snap, loading: %s', filename ) + return self._load_and_clean( filename ) def _load_and_clean( self, filename ): """Use pygame to load given filename, delete after loading/attempt""" try: @@ -135,33 +142,23 @@ class Camera(object): token -- passed back as attribute of the event which signals that capture is finished - We return events of type CAMERA_LOAD with an attribute "succeed" + We return two types of events CAMERA_LOAD and CAMERA_LOAD_FAIL, depending on whether we succeed or not. Attributes of the events which are returned: - success -- whether the loading process succeeded token -- as passed to this method - image -- pygame image.load result if successful, None otherwise filename -- the filename in our temporary directory we used to store - the file temporarily (this file will be deleted before the event - is sent, the name is for informational purposes only). + the file temporarily + image -- pygame image.load result if successful, None otherwise err -- Exception instance if failed, None otherwise Basically identical to the snap method, save that it posts a message - to the event bus in pygame.event instead of blocking and returning... - - Example: - if event == pygame.MOUSEBUTTONDOWN: - camera = Camera( source='test', filename = 'picture32' ) - camera.snap_async( myIdentifier ) - ... - elif event.type == olpcgames.CAMERA_LOAD: - if event.token == myIdentifier: - doSomething( event.image ) + to the event bus in eventwrap instead of blocking and returning... """ log.debug( 'beginning async snap') - t = threading.Thread(target=self._background_snap, args=[token]) + t = threading.Thread(target=self._background_snap, args=(token,)) t.start() + log.debug( 'background thread started for gstreamer' ) return token def _background_snap( @@ -178,33 +175,50 @@ class Camera(object): we begin playing, the second for when we finish. """ log.debug( 'Background thread kicking off gstreamer capture begun' ) - from pygame.event import Event, post - filename, pipe = self._create_subprocess() - if not pipe.wait(): - success = True - log.debug( 'Ending capture, loading: %s', filename ) - try: - image = self._load_and_clean( filename ) - except Exception, err: - image = None - success = False - else: - err = None - else: - success = False - err = pipe.stderr.read() - image = None - evt = Event( - CAMERA_LOAD, - dict( - filename=filename, - success = success, - token = token, - image=image, - err=err - ) - ) - post( evt ) + from olpcgames import eventwrap + from pygame.event import Event + filename, pipe = self._create_pipe() + bus = pipe.get_bus() + bus.add_signal_watch() + def _background_snap_onmessage( bus, message ): + """Handle messages from the picture-snapping bus""" + log.debug( 'Message handler for gst messages: %s', message ) + t = message.type + if t == gst.MESSAGE_EOS: + pipe.set_state(gst.STATE_NULL) + try: + image = self._load_and_clean( filename ) + success = True + except Exception, err: + success = False + image = None + else: + err = None + log.debug( 'Success loading file %r', token ) + eventwrap.post(Event( + CAMERA_LOAD, + filename=filename, + success = success, + token = token, + image=image, + err=err + )) + + elif t == gst.MESSAGE_ERROR: + log.warn( 'Failure loading file %r: %s', token, message ) + pipe.set_state(gst.STATE_NULL) + err, debug = message.parse_error() + eventwrap.post(Event( + CAMERA_LOAD_FAIL, + filename=filename, + success = False, + token = token, + image=None, + err=err + )) + return False + bus.connect('message', _background_snap_onmessage) + pipe.set_state(gst.STATE_PLAYING) def snap(): """Dump a snapshot from the camera to a pygame surface in background thread diff --git a/olpcgames/canvas.py b/olpcgames/canvas.py index 2583827..0874d2d 100755..100644 --- a/olpcgames/canvas.py +++ b/olpcgames/canvas.py @@ -1,9 +1,9 @@ -"""Implements bridge connection between Sugar/GTK and Pygame""" +"""Implements bridge connection between Sugar/GTK and PyGame""" import os import sys import logging log = logging.getLogger( 'olpcgames.canvas' ) -##log.setLevel( logging.DEBUG ) +#log.setLevel( logging.DEBUG ) import threading from pprint import pprint @@ -15,31 +15,29 @@ import pygame from olpcgames import gtkEvent, util -__all__ = ['PygameCanvas'] +__all__ = ['PyGameCanvas'] -class PygameCanvas(gtk.Layout): - """Canvas providing bridge methods to run Pygame in GTK +class PyGameCanvas(gtk.Layout): + """Canvas providing bridge methods to run PyGame in GTK - The PygameCanvas creates a secondary thread in which the Pygame instance will - live, providing synthetic Pygame events to that thread via a Queue. The GUI - connection is done by having the Pygame canvas use a GTK Port object as it's + The PyGameCanvas creates a secondary thread in which the Pygame instance will + live, providing synthetic PyGame events to that thread via a Queue. The GUI + connection is done by having the PyGame canvas use a GTK Port object as it's window pointer, it draws to that X-level window in order to produce output. """ - mod_name = None def __init__(self, width, height): """Initializes the Canvas Object width,height -- passed to the inner EventBox in order to request a given size, - the Socket is the only child of this EventBox, and the Pygame commands + the Socket is the only child of this EventBox, and the PyGame commands will be writing to the Window ID of the socket. The internal EventBox is - centered via an Alignment instance within the PygameCanvas instance. + centered via an Alignment instance within the PyGameCanvas instance. XXX Should refactor so that the internal setup can be controlled by the sub-class, e.g. to get size from the host window, or something similar. """ # Build the main widget - log.info( 'Creating the pygame canvas' ) - super(PygameCanvas,self).__init__() + super(PyGameCanvas,self).__init__() self.set_flags(gtk.CAN_FOCUS) # Build the sub-widgets @@ -75,11 +73,10 @@ class PygameCanvas(gtk.Layout): Side effects: Sets the SDL_WINDOWID variable to our socket's window ID - Calls Pygame init - Causes the gtkEvent.Translator to "hook" Pygame - Creates and starts secondary thread for Game/Pygame event processing. + Calls PyGame init + Causes the gtkEvent.Translator to "hook" PyGame + Creates and starts secondary thread for Game/PyGame event processing. """ - log.info( 'Connecting the pygame canvas' ) # Setup the embedding os.environ['SDL_WINDOWID'] = str(self._socket.get_id()) #print 'Socket ID=%s'%os.environ['SDL_WINDOWID'] @@ -92,7 +89,6 @@ class PygameCanvas(gtk.Layout): if ':' not in app: app += ':main' mod_name, fn_name = app.split(':') - self.mod_name = mod_name mod = __import__(mod_name, globals(), locals(), []) fn = getattr(mod, fn_name) @@ -102,7 +98,6 @@ class PygameCanvas(gtk.Layout): def _start(self, fn): """The method that actually runs in the background thread""" - log.info( 'Staring the mainloop' ) import olpcgames olpcgames.widget = olpcgames.WIDGET = self try: @@ -117,55 +112,13 @@ class PygameCanvas(gtk.Layout): try: try: - try: - log.info( '''Running mainloop: %s''', fn ) - fn() - except Exception, err: - log.error( - """Uncaught top-level exception: %s""", - util.get_traceback( err ), - ) - raise - else: - log.info( "Mainloop exited" ) - finally: - log.debug( "Clearing any pending events" ) - from olpcgames import eventwrap - eventwrap.clear() + log.info( '''Running mainloop: %s''', fn ) + fn() + except Exception, err: + log.error( + """Uncaught top-level exception: %s""", + util.get_traceback( err ), + ) + raise finally: - log.info( 'Main function finished, calling main_quit' ) gtk.main_quit() - - source_object_id = None - def view_source(self): - """Implement the 'view source' key by saving - datastore, and then telling the Journal to view it.""" - if self.source_object_id is None: - from sugar import profile - from sugar.datastore import datastore - from sugar.activity.activity import get_bundle_name, get_bundle_path - from gettext import gettext as _ - import os.path - jobject = datastore.create() - metadata = { - 'title': _('%s Source') % get_bundle_name(), - 'title_set_by_user': '1', - 'suggested_filename': 'pippy_app.py', - 'icon-color': profile.get_color().to_string(), - 'mime_type': 'text/x-python', - } - for k,v in metadata.items(): - jobject.metadata[k] = v # dict.update method is missing =( - jobject.file_path = os.path.join(get_bundle_path(), 'pippy_app.py') - datastore.write(jobject) - self.__source_object_id = jobject.object_id - jobject.destroy() - self.journal_show_object(self.__source_object_id) - def journal_show_object(self, object_id): - """Invoke journal_show_object from sugar.activity.activity if it - exists.""" - try: - from sugar.activity.activity import show_object_in_journal - show_object_in_journal(object_id) - except ImportError: - pass # no love from sugar. diff --git a/olpcgames/data/__init__.py b/olpcgames/data/__init__.py index 8510186..8510186 100755..100644 --- a/olpcgames/data/__init__.py +++ b/olpcgames/data/__init__.py diff --git a/olpcgames/data/sleeping_svg.py b/olpcgames/data/sleeping_svg.py index c52398a..fa67eee 100755..100644 --- a/olpcgames/data/sleeping_svg.py +++ b/olpcgames/data/sleeping_svg.py @@ -3,59 +3,499 @@ # written by resourcepackage: (1, 0, 1) source = 'sleeping.svg' package = 'olpcgames.data' -data = "\012\012 \012 \012 \012 \012 \012 \ -\012 \012 \012 \012 \012 \012 \012 \012 \012 \012\012" +data = "\012\012\012 \012 \012 \012 \012 \ +\012 \012 \ +\012 \012 \012 \012 \012 \ + \012 image/s\ +vg+xml\012 \012 \ +\012 \012 \012 \012 \012 \012 \012 \012 \012 \012\012" ### end diff --git a/olpcgames/dbusproxy.py b/olpcgames/dbusproxy.py deleted file mode 100755 index a103e28..0000000 --- a/olpcgames/dbusproxy.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Spike test for a safer networking system for DBUS-based objects""" -from olpcgames import eventwrap, util -from dbus import proxies -import logging -log = logging.getLogger( 'dbus' ) -log.setLevel( logging.DEBUG ) - -def wrap( value, tube=None,path=None ): - """Wrap object with any required pygame-side proxies""" - if isinstance( value,proxies._ProxyMethod ): - return DBUSMethod( value, tube=tube, path=path ) - elif isinstance( value, proxies._DeferredMethod ): - value._proxy_method = DBUSMethod( value._proxy_method, tube=tube, path=path ) - return value - elif isinstance( value, proxies.ProxyObject ): - return DBUSProxy( value, tube=tube, path=path ) - else: - return value - -class DBUSProxy( object ): - """Proxy for the DBUS Proxy object""" - def __init__( self, proxy, tube=None, path=None ): - log.info( 'Creating Pygame-side proxy for %s (%s)', proxy,path ) - self.__proxy = proxy - self.__tube = tube - self.__path = path - def __getattr__( self, key ): - """Retrieve attribute of given key""" - from dbus import proxies - return wrap( getattr( self.__proxy, key ) ) - def add_signal_receiver( self, callback, eventName, interface, path=None, sender_keyword='sender'): - """Add a new signal handler (which will be called many times) for given signal - """ - log.info( """Setting signal receiver %s for event %s on interface %s (object path %s) with sender_keyword = %r""", - callback, eventName, interface, path, sender_keyword, - ) - log.debug( """proxy: %s proxy.tube: %s""", self.__proxy, self.__proxy.tube ) - self.__tube.add_signal_receiver( - Callback( callback ), - eventName, - interface, - path = path or self.__path, - sender_keyword = sender_keyword, - ) - -class DBUSMethod( object ): - """DBUS method which does callbacks in the Pygame (eventwrapper) thread""" - def __init__( self, proxy, tube,path ): - log.info( 'Creating Pygame-side method proxy for %s', proxy ) - self.__proxy = proxy - self.__tube = tube - self.__path = path - def __call__( self, *args, **named ): - """Perform the asynchronous call""" - log.info( 'Calling proxy for %s with *%s, **%s', self.__proxy, args, named ) - callback, errback = named.get( 'reply_handler'), named.get( 'error_handler' ) - if not callback: - raise TypeError( """Require a reply_handler named argument to do any asynchronous call""" ) - else: - callback = Callback( callback ) - if not errback: - errback = defaultErrback - else: - errback = Callback( errback ) - named['reply_handler'] = callback - named['error_handler'] = errback - return self.__proxy( *args, **named ) - -def defaultErrback( error ): - """Log the error to stderr/log""" - log.error( """Failure in DBUS call: %s""", error ) - -class Callback( object ): - """PyGTK-side callback which generates a CallbackResult to process on the Pygame side""" - def __init__( self, callable, callContext = None): - """Initialize the callback to process results""" - self.callable = callable - if callContext is None: - callContext = util.get_traceback( None ) - self.callContext = callContext - def __call__( self, *args, **named ): - """PyGTK-side callback operation""" - log.info( 'Callback %s return value *%s, **%s', self.callable, args, named ) - from olpcgames import eventwrap - args = [wrap(a) for a in args] - named = dict([ - (k,wrap(v)) for k,v in named.items() - ]) - eventwrap.post( - eventwrap.CallbackResult( - self.callable, args, named, callContext = self.callContext - ) - ) diff --git a/olpcgames/eventwrap.py b/olpcgames/eventwrap.py index 402109c..92fbabb 100755..100644 --- a/olpcgames/eventwrap.py +++ b/olpcgames/eventwrap.py @@ -4,131 +4,32 @@ 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. +Extension: -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... + last_event_time() -- returns period since the last event was produced + in seconds. This can be used to create "pausing" effects for games. """ import pygame import gtk import Queue -import thread, threading +import thread import logging -from olpcgames import util log = logging.getLogger( 'olpcgames.eventwrap' ) +# This module reuses Pygame's Event, but +# reimplements the event queue. from pygame.event import Event, event_name, pump as pygame_pump, get as pygame_get class Event(object): """Mock pygame events""" - def __init__(self, type, dict=None,**named): - """Initialise the new event variables from dictionary and named become attributes""" + def __init__(self, type, **named): self.type = type - if dict: - self.__dict__.update( dict ) self.__dict__.update( named ) - def _get_dict( self ): - return self.__dict__ - dict = property( _get_dict ) - def __repr__( self ): - result = [] - for key,value in self.__dict__.items(): - if not key.startswith( '_' ): - result.append( '%s = %r'%( key, value )) - return '%s( %s, %s )'%( - self.__class__.__name__, - self.type, - ",".join( result ), - ) - def block( self ): - """Block until this event is finished processing - - Event process is only finalized on the *next* call to retrieve an event - after the processing operation in which the event is processed. In some - extremely rare cases we might actually see that happen, were the - file-saving event (for example) causes the Pygame event loop to exit. - In that case, the GTK event loop *could* hang. - """ - log.info( '''Blocking GTK thread on event: %s''', self ) - self.__lock = threading.Event() - self.__lock.wait() - def retire( self ): - """Block the GTK event loop until this event is processed""" - try: - self.__lock.set() - log.info( '''Released GTK thread on event: %s''', self ) - except AttributeError, err: - pass - -class CallbackResult( object ): - def __init__( self, callable, args, named, callContext=None ): - """Perform callback in Pygame loop with args and named - - callContext is used to provide more information when there is - a failure in the callback (for debugging purposes) - """ - self.callable = callable - self.args = args - self.named = named - if callContext is None: - callContext = util.get_traceback( None ) - self.callContext = callContext - def __call__( self ): - """Perform the actual callback in the Pygame event loop""" - try: - self.callable( *self.args, **self.named ) - except Exception, err: - log.error( - """Failure in callback %s( *%s, **%s ): %s\n%s""", - getattr(self.callable, '__name__',self.callable), - self.args, self.named, - util.get_traceback( err ), - self.callContext - ) - -_EVENTS_TO_RETIRE = [] - -def _releaseEvents( ): - """Release/retire previously-processed events""" - if _EVENTS_TO_RETIRE: - for event in _EVENTS_TO_RETIRE: - try: - event.retire() - except AttributeError, err: - pass - -def _processCallbacks( events ): - """Process any callbacks in events and remove from the stream""" - result = [] - for event in events: - if isinstance( event, CallbackResult ): - event() - else: - result.append( event ) - if events and not result: - result.append( - Event( type=pygame.NOEVENT ) - ) - return result - -def _recordEvents( events ): - """Record the set of events to retire on the next iteration""" - global _EVENTS_TO_RETIRE - events = _processCallbacks( events ) - _EVENTS_TO_RETIRE = events - return events +#print "Initializing own event.py" +# Install myself on top of pygame.event def install(): """Installs this module (eventwrap) as an in-place replacement for the pygame.event module. @@ -140,122 +41,31 @@ def install(): registers the handler itself, so you will always wind up with it registered when you use olpcgames (the gtkEvent.Translator.hook_pygame method calls it). """ - log.info( 'Installing OLPCGames event wrapper' ) - from olpcgames import eventwrap - import pygame + import eventwrap,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, err: - return None # none yet, at least - -g_events = _FilterQueue() +g_events = Queue.Queue() # Set of blocked events as set by set g_blocked = set() -g_blockedlock = thread.allocate_lock() # should use threading instead +g_blockedlock = thread.allocate_lock() g_blockAll = False -def _typeChecker( types ): - """Create check whether an event is in types""" - try: - if 1 in types: - pass - def check( element ): - return element.type in types - return check - except TypeError, err: - def check( element ): - return element.type == types - return check - def pump(): - """Handle any window manager and other external events that aren't passed to the user - - Call this periodically (once a frame) if you don't call get(), poll() or wait() - """ + """Handle any window manager and other external events that aren't passed to the user. Call this periodically (once a frame) if you don't call get(), poll() or wait().""" pygame_pump() - _releaseEvents() -def get( 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! - """ +def get(): + """Get a list of all pending events. (Unlike pygame, there's no option to filter by event type; you should use set_blocked() if you don't want to see certain events.)""" 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)) + while True: + eventlist.append(g_events.get(block=False)) except Queue.Empty: pass @@ -263,14 +73,32 @@ def get( types=None): if pygameEvents: log.info( 'Raw Pygame events: %s', pygameEvents) eventlist.extend( pygameEvents ) - return _recordEvents( eventlist ) + if eventlist: + _set_last_event_time() + return eventlist + +_LAST_EVENT_TIME = 0 + +def _set_last_event_time( time=None ): + """Set this as the last event time""" + global _LAST_EVENT_TIME + if time is None: + time = pygame.time.get_ticks() + _LAST_EVENT_TIME = time + return time + +def last_event_time( ): + """Return the last event type for pausing operations in seconds""" + global _LAST_EVENT_TIME + return (pygame.time.get_ticks() - _LAST_EVENT_TIME)/1000. 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] + _set_last_event_time() + return result except Queue.Empty: return Event(pygame.NOEVENT) @@ -279,49 +107,30 @@ 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. + do not find an event before then, return None """ pump() try: - result = None result = g_events.get(block=True, timeout=timeout) - try: - return _recordEvents( [result] )[0] - except IndexError, err: - return Event( type=pygame.NOEVENT ) + _set_last_event_time() + return result except Queue.Empty, err: 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 + """True if there is any pending event. (Unlike pygame, there's no option to + filter by event type)""" return not g_events.empty() def clear(): - """Clears the entire pending queue of events - - Rarely used - """ + """Dunno why you would do this, but throws every event out of the queue""" try: - discarded = [] while True: - discarded.append( g_events.get(block=False) ) - discarded = _recordEvents( discarded ) - _releaseEvents() - return discarded + g_events.get(block=False) 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 @@ -331,7 +140,6 @@ def set_blocked(item): g_blockedlock.release() def set_allowed(item): - """Allow item/items to be added to the event queue""" g_blockedlock.acquire() try: if item is None: @@ -352,16 +160,18 @@ def get_blocked(*args, **kwargs): g_blockedlock.release() def set_grab(grabbing): - """This method will not be implemented""" + # We don't do this. + pass def get_grab(): - """This method will not be implemented""" + # We don't do this. + return False def post(event): - """Post a new event to the Queue of events""" + #print "posting on own" g_blockedlock.acquire() try: - if getattr(event,'type',None) not in g_blocked: + if event.type not in g_blocked: g_events.put(event, block=False) finally: g_blockedlock.release() diff --git a/olpcgames/gtkEvent.py b/olpcgames/gtkEvent.py index 6b20102..ce4f9eb 100755..100644 --- a/olpcgames/gtkEvent.py +++ b/olpcgames/gtkEvent.py @@ -7,7 +7,7 @@ import pygame from olpcgames import eventwrap import logging log = logging.getLogger( 'olpcgames.gtkevent' ) -##log.setLevel( logging.DEBUG ) +#log.setLevel( logging.DEBUG ) class _MockEvent(object): """Used to inject key-repeat events on the gtk side.""" @@ -98,29 +98,13 @@ class Translator(object): self.__tick_id = None #print "translator initialized" - self._inner_evb.connect( 'expose-event', self.do_expose_event ) -# screen = gtk.gdk.screen_get_default() -# screen.connect( 'size-changed', self.do_resize_event ) - self._inner_evb.connect( 'configure-event', self.do_resize_event ) + mainwindow.connect( 'expose-event', self.do_expose_event ) def do_expose_event(self, event, widget): """Handle exposure event (trigger redraw by gst)""" log.info( 'Expose event: %s', event ) from olpcgames import eventwrap eventwrap.post( eventwrap.Event( eventwrap.pygame.VIDEOEXPOSE )) return True - def do_resize_event( self, activity, event ): - """Our screen (actually, the default screen) has resized""" - log.info( 'Resize event: %s %s', activity, event ) - log.info( 'Event values: %s', (event.width,event.height) ) -# from olpcgames import eventwrap -# # shouldn't the activity's window have this information too? -# eventwrap.post( -# eventwrap.Event( -# eventwrap.pygame.VIDEORESIZE, -# dict(size=(event.width,event.height), width=event.width, height=event.height) -# ) -# ) - return False # continue processing def hook_pygame(self): """Hook the various Pygame features so that we implement the event APIs""" # Pygame should be initialized. Hijack their key and mouse methods @@ -180,9 +164,6 @@ class Translator(object): keycode = getattr(pygame, 'K_'+key.upper()) elif hasattr(pygame, 'K_'+key.lower()): keycode = getattr(pygame, 'K_'+key.lower()) - elif key == 'XF86Start': - # view source request, specially handled... - self._mainwindow.view_source() else: print 'Key %s unrecognized'%key diff --git a/olpcgames/mesh.py b/olpcgames/mesh.py index 1ad4c43..254089f 100755..100644 --- a/olpcgames/mesh.py +++ b/olpcgames/mesh.py @@ -1,125 +1,7 @@ -'''Utilities for wrapping the telepathy network for Pygame - -The 'mesh' module allows your Pygame game to be Shared -across the OLPC networking infrastructure (D-bus and Tubes). -It offers a simplified view of the Telepathy system. - -All Sugar activities have a 'Share' menu (toolbar) which is -intended to allow other people to join the activity instance -and collaborate with you. When you select Share, the activity's -icon appears on the Neighborhood view of other laptops. - -If you do nothing else with networking, this is all that will -happen: if anyone selects your shared activity icon, they will -just spawn a new instance of the activity, and they will get to -play your game alone. - -The mesh module automatically sets up a connection from each -participant to every other participant. It provides (string based) -communications channels that let you either broadcast messages -to other users or communicate point-to-point to one other user. - -You can use the "handles" which uniquely idenify users to send -messages to an individual user (send_to( handle, message )) or -broadcast( message ) to send a message to all participants. - -More advanced (structured) networking can be handled by using -the get_object( handle, path ) function, which looks up an object -(by DBUS path) shared by the user "handle" and returns a -DBUS/Telepathy proxy for that object. The object you get back is -actually an olpcgames.dbusproxy.DBUSProxy instance, which -enforces asynchronous operations and runs your -reply_handler/error_handler in the Pygame event loop. - -NOTE: - You *cannot* make synchronous calls on these objects! - You must use the named arguments: - - reply_handler, error_handler - - for every call which you perform on a shared object (normally - these are ExportedGObject instances). - -If you want to run your callbacks in the GTK event loop (for instance -because they need to handle GTK-side objects), you can use the -dbus_get_object function. This is *not* recommended for normal -usage, as any call to Pygame operations within the GTK event loop -can cause a segfault/core of your entire Activity. - -Note: - - mesh sets up N**2 connections for each shared activity, obviously - that will not scale to very large shared activities. - -Note: - - The intention is that mesh will be refactored, possibly as a - new module called "olpcgames.network", which would break out - the various components so that there is no longer an assumed - networking layout. We will attempt to retain the mesh module's - API as we do so. - -Events produced: - - olpcgames.CONNECT -- The tube connection was started. (i.e., the - user clicked Share or started the activity from the Neighborhood - screen). - - Event properties: - - id -- a unique identifier for this connection. (shouldn't be needed - for anything) - - olpcgames.PARTICIPANT_ADD -- A participant joined the activity. - This will trigger for the local user as well as any arriving remote - users. Note that this *only* occurs after the activity is shared, - that is, the local user does not appear until after they have - shared a locally-started activity. - - Event properties: - - handle -- the arriving user's handle (a uniquely identifying string - assigned to the user by the Telepathy system, not human - readable), see lookup_buddy to retrieve human-readable - descriptions of the user. - - olpcgames.PARTICIPANT_REMOVE -- A participant quit the activity. - - Event properties: - - handle -- the departing user's handle. - - olpcgames.MESSAGE_UNI -- A message was sent to you. - - Event properties: - - content -- the content of the message (a string) - handle -- the handle of the sending user. - - olpcgames.MESSAGE_MULTI -- A message was sent to everyone. - - Event properties: - - content -- the content of the message (a string) - handle -- the handle of the sending user. - -Note: - - Eventually we will stop using top-level Pygame event types for the - various networking message types (currently four of them). We will - likely use UserEvent with a sub-type specifier for the various events - that OLPCGames produces. - -See Also: - - http://blog.vrplumber.com/2016 -- Discussion of how Productive uses - the mesh module and raw Telepathy (ExportedGObject instances) -''' +'''mesh.py: utilities for wrapping the mesh and making it accessible to Pygame''' import logging log = logging.getLogger( 'olpcgames.mesh' ) -##log.setLevel( logging.DEBUG ) -import olpcgames -from olpcgames.util import get_traceback +#log.setLevel( logging.DEBUG ) try: from sugar.presence.tubeconn import TubeConnection except ImportError, err: @@ -135,12 +17,6 @@ try: except ImportError, err: telepathy = None -try: - import sugar.presence.presenceservice -except Exception, err: - pass -import pygame.event as PEvent - class OfflineError( Exception ): """Raised when we cannot complete an operation due to being offline""" @@ -151,11 +27,34 @@ DBUS_SERVICE = None ### NEW PYGAME EVENTS ### -CONNECT = olpcgames.CONNECT -PARTICIPANT_ADD = olpcgames.PARTICIPANT_ADD -PARTICIPANT_REMOVE = olpcgames.PARTICIPANT_REMOVE -MESSAGE_UNI = olpcgames.MESSAGE_UNI -MESSAGE_MULTI = olpcgames.MESSAGE_MULTI +'''The tube connection was started. (i.e., the user clicked Share or started +the activity from the Neighborhood screen). +Event properties: + id: a unique identifier for this connection. (shouldn't be needed for anything)''' +CONNECT = 9912 + +'''A participant joined the activity. This will trigger for the local user +as well as any arriving remote users. +Event properties: + handle: the arriving user's handle.''' +PARTICIPANT_ADD = 9913 + +'''A participant quit the activity. +Event properties: + handle: the departing user's handle.''' +PARTICIPANT_REMOVE = 9914 + +'''A message was sent to you. +Event properties: + content: the content of the message (a string) + handle: the handle of the sending user.''' +MESSAGE_UNI = 9915 + +'''A message was sent to everyone. +Event properties: + content: the content of the message (a string) + handle: the handle of the sending user.''' +MESSAGE_MULTI = 9916 # Private objects for useful purposes! @@ -234,23 +133,13 @@ def activity_joined(activity): if connect_callback is not None: connect_callback() -def _getConn( activity ): - log.debug( '_getConn' ) +def _getConn(): + log.info( '_getConn' ) + pservice = _get_presence_service() + name, path = pservice.get_preferred_connection() global conn - if conn: - return conn - else: - if hasattr( activity._shared_activity, 'telepathy_conn' ): - log.debug( '''new-style api for retrieving telepathy connection present''' ) - conn = activity._shared_activity.telepathy_conn - else: - pservice = _get_presence_service() - log.debug( '_get_presence_service -> %s', pservice ) - name, path = pservice.get_preferred_connection() - log.debug( '_get_presence_service -> %s, %s', name, path) - conn = telepathy.client.Connection(name, path) - log.debug( 'Telepathy Client Connection: %s', conn ) - return conn + conn = telepathy.client.Connection(name, path) + return conn @@ -260,48 +149,37 @@ channel present, creates one. Updates text_chan and tubes_chan. setup(sugar.activity.Activity, telepathy.client.Connection)''' global text_chan, tubes_chan, DBUS_SERVICE - log.info( 'Setup for %s', activity ) if not DBUS_SERVICE: DBUS_SERVICE = activity.get_bundle_id() if not activity.get_shared(): log.error('Failed to share or join activity') raise "Failure" - if hasattr( activity._shared_activity, 'telepathy_tubes_chan' ): - log.debug( '''Improved channel setup API available''' ) - _getConn( activity ) - conn = activity._shared_activity.telepathy_conn - tubes_chan = activity._shared_activity.telepathy_tubes_chan - text_chan = activity._shared_activity.telepathy_text_chan - else: - log.debug( '''Old-style setup API''' ) - bus_name, conn_path, channel_paths = activity._shared_activity.get_channels() - _getConn( activity ) - - # Work out what our room is called and whether we have Tubes already - room = None - tubes_chan = None - text_chan = None - for channel_path in channel_paths: - log.debug( 'Testing channel path: %s', channel_path) - channel = telepathy.client.Channel(bus_name, channel_path) - htype, handle = channel.GetHandle() - log.debug( ' Handle Type: %s Handle: %s', htype, handle) - if htype == telepathy.HANDLE_TYPE_ROOM: - log.debug('Found our room: it has handle#%d "%s"', - handle, conn.InspectHandles(htype, [handle])[0]) - room = handle - ctype = channel.GetChannelType() - if ctype == telepathy.CHANNEL_TYPE_TUBES: - log.debug('Found our Tubes channel at %s', channel_path) - tubes_chan = channel - elif ctype == telepathy.CHANNEL_TYPE_TEXT: - log.debug('Found our Text channel at %s', channel_path) - text_chan = channel - - if room is None: - log.error("Presence service didn't create a room") - raise "Failure" + bus_name, conn_path, channel_paths = activity._shared_activity.get_channels() + _getConn() + + # Work out what our room is called and whether we have Tubes already + room = None + tubes_chan = None + text_chan = None + for channel_path in channel_paths: + channel = telepathy.client.Channel(bus_name, channel_path) + htype, handle = channel.GetHandle() + if htype == telepathy.HANDLE_TYPE_ROOM: + log.debug('Found our room: it has handle#%d "%s"', + handle, conn.InspectHandles(htype, [handle])[0]) + room = handle + ctype = channel.GetChannelType() + if ctype == telepathy.CHANNEL_TYPE_TUBES: + log.debug('Found our Tubes channel at %s', channel_path) + tubes_chan = channel + elif ctype == telepathy.CHANNEL_TYPE_TEXT: + log.debug('Found our Text channel at %s', channel_path) + text_chan = channel + + if room is None: + log.error("Presence service didn't create a room") + raise "Failure" if text_chan is None: log.error("Presence service didn't create a text channel") raise "Failure" @@ -315,7 +193,6 @@ setup(sugar.activity.Activity, telepathy.client.Connection)''' tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', new_tube_cb) - log.info( 'Setup for %s complete', activity ) return (text_chan, tubes_chan) def new_tube_cb(id, initiator, type, service, params, state): @@ -343,69 +220,32 @@ def _list_tubes_reply_cb(tubes): def _list_tubes_error_cb(e): log.error('ListTubes() failed: %s', e) -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 - """ + + +def get_buddy(dbus_handle): + """Get a Buddy from a handle.""" log.debug('Trying to find owner of handle %s...', dbus_handle) cs_handle = instance().tube.bus_name_to_handle[dbus_handle] log.debug('Trying to find my handle in %s...', cs_handle) group = text_chan[telepathy.CHANNEL_INTERFACE_GROUP] log.debug( 'Calling GetSelfHandle' ) - if not errback: - def errback( error ): - log.error( """Failure retrieving handle for buddy lookup: %s""", error ) - def with_my_csh( my_csh ): - log.debug('My handle in that group is %s', my_csh) - def _withHandle( handle ): - """process the results of the handle values""" - # XXX: we're assuming that we have Buddy objects for all contacts - - # this might break when the server becomes scalable. - pservice = _get_presence_service() - name, path = pservice.get_preferred_connection() - callback( pservice.get_buddy_by_telepathy_handle(name, path, handle) ) - if my_csh == cs_handle: - conn.GetSelfHandle(reply_handler = _withHandle, error_handler=errback) - log.debug('CS handle %s belongs to me, looking up with GetSelfHandle', cs_handle) - elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: - handle = group.GetHandleOwners([cs_handle])[0] - log.debug('CS handle %s belongs to %s', cs_handle, handle) - _withHandle( handle ) - else: - handle = cs_handle - log.debug('non-CS handle %s belongs to itself', handle) - _withHandle( handle ) - group.GetSelfHandle( reply_handler = with_my_csh, error_handler = errback) - - + my_csh = group.GetSelfHandle() + log.debug('My handle in that group is %s', my_csh) + if my_csh == cs_handle: + handle = conn.GetSelfHandle() + log.debug('CS handle %s belongs to me, %s', cs_handle, handle) + elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + log.debug('CS handle %s belongs to %s', cs_handle, handle) + else: + handle = cs_handle + log.debug('non-CS handle %s belongs to itself', handle) -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.""" - ) + # XXX: we're assuming that we have Buddy objects for all contacts - + # this might break when the server becomes scalable. + pservice = _get_presence_service() + name, path = pservice.get_preferred_connection() + return pservice.get_buddy_by_telepathy_handle(name, path, handle) def _get_presence_service( ): """Attempt to retrieve the presence service (check for offline condition) @@ -413,25 +253,20 @@ def _get_presence_service( ): The presence service, when offline, has no preferred connection type, so we check that before returning the object... """ - log.debug( """About to import sugar.presence.presenceservice""" ) + import sugar.presence.presenceservice + pservice = sugar.presence.presenceservice.get_instance() try: - log.debug( 'About to retrieve presence service instance' ) - pservice = sugar.presence.presenceservice.get_instance() - try: - log.debug( ' Retrieved presence service instance: %s', pservice ) - name, path = pservice.get_preferred_connection() - log.debug( ' Name = %s Path = %s', name, path ) - except (TypeError,ValueError), err: - log.warn('Working in offline mode, cannot retrieve buddy information for %s: %s', handle, err ) - raise OfflineError( """Unable to retrieve buddy information, currently offline""" ) - else: - return pservice - except Exception, err: - log.error( """Failure in _get_presence_service: %s""", get_traceback( err )) + name, path = pservice.get_preferred_connection() + except (TypeError,ValueError), err: + log.warn('Working in offline mode, cannot retrieve buddy information for %s: %s', handle, err ) + raise OfflineError( """Unable to retrieve buddy information, currently offline""" ) + else: + return pservice def instance(idx=0): return pygametubes[idx] +import eventwrap,pygame.event as PEvent class PygameTube(ExportedGObject): '''The object whose instance is shared across D-bus @@ -446,7 +281,7 @@ class PygameTube(ExportedGObject): self.is_initiator = is_initiator self.entered = False self.ordered_bus_names = [] - PEvent.post(PEvent.Event(CONNECT, id=tube_id)) + eventwrap.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) @@ -456,15 +291,21 @@ class PygameTube(ExportedGObject): def participant_change_cb(self, added, removed): log.debug( 'participant_change_cb: %s %s', added, removed ) + def nick(buddy): + if buddy is not None: + return buddy.props.nick + else: + return 'Unknown' + 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)) + eventwrap.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)) + eventwrap.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle)) if self.is_initiator: if not self.entered: @@ -489,11 +330,11 @@ class PygameTube(ExportedGObject): @method(dbus_interface=DBUS_IFACE, in_signature='s', out_signature='', sender_keyword='sender') def Tell(self, content, sender=None): '''This is the targeted-message interface; called when a message is received that was sent directly to me.''' - PEvent.post(PEvent.Event(MESSAGE_UNI, handle=sender, content=content)) + eventwrap.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)) + eventwrap.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.''' @@ -542,42 +383,16 @@ def get_participants(): except IndexError, err: 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. - ''' - if warning: - log.warn( 'Use of dbus_get_object is only safe from the GTK mainloop, use dbus_get_object_proxy instead: %s %s', handle, path ) - return instance().tube.get_object(handle, path) - -def get_object(handle, path): - '''Get a D-BUS proxy object from another participant for use in Pygame +def dbus_get_object(handle, path): + '''Get a D-bus object from another participant. 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. + yourself. Simply define a D-bus class with an interface and path that you choose; when you want a reference to the corresponding remote object on a participant, call this method. - - returns an olpcgames.dbusproxy.DBUSProxy( ) object wrapping - the DBUSProxy object. - - The dbus_get_object_proxy name is deprecated ''' - log.debug( 'DBUS get_object( %r %r )', handle, path ) - from olpcgames import dbusproxy - return dbusproxy.DBUSProxy( - instance().tube.get_object( handle, path), - tube=instance().tube, - path=path - ) - -dbus_get_object_proxy = get_object + log.debug( 'dbus_get_object: %s %s', handle, path ) + return instance().tube.get_object(handle, path) diff --git a/olpcgames/pangofont.py b/olpcgames/pangofont.py index 441dfd1..81a2d7c 100755..100644 --- a/olpcgames/pangofont.py +++ b/olpcgames/pangofont.py @@ -5,56 +5,20 @@ Depends on: pygtk (to get the pango context) pycairo (for the pango rendering context) python-pango (obviously) - numpy - (pygame) - -As soon as you import this module you have loaded *all* of the above. -You can still use pygame.font until you decide to call install(), which -will replace pygame.font with this module. - -Notes: - - * no ability to load TTF files, PangoFont uses the font files registered - with GTK/X to render graphics, it cannot load an arbitrary TTF file. - Most non-Sugar Pygame games use bundled TTF files, which means - that you will likely need at least some changes to your font handling. - - Note, however, that the Pygame Font class is available to load the TTF - files, so if you don't want to take advantage of PangoFont for already - written code, but want to use it for "system font" operations, you can - mix the two. - - * metrics are missing, Pango can provide the information, but the more - involved metrics system means that translating to the simplified model - in Pygame has as of yet not been accomplished. - - * better support for "exotic" languages and scripts (which is why we use it) - -The main problem with SDL_ttf is that it doesn't handle internationalization -nearly as well as Pango (in fact, pretty much nothing does). However, it is -fairly fast and it has a rich interface. You should avoid fonts where possible, -prerender using Pango for internationalizable text, and use Pango or SDL_ttf -for text that really needs to be rerendered each frame. (Use SDL_ttf if profiling -demonstrates that performance is poor with Pango.) - -Note: - Font -- is the original Pygame Font class, which allows you to load - fonts from TTF files/filenames - PangoFont -- is the Pango-specific rendering engine which allows - for the more involved cross-lingual rendering operations. + pygame (obviously) """ import pango import logging +import cairo import pangocairo import pygame.rect, pygame.image import gtk import struct from pygame import surface -from pygame.font import Font from olpcgames import _cairoimage log = logging.getLogger( 'olpcgames.pangofont' ) -##log.setLevel( logging.DEBUG ) +#log.setLevel( logging.DEBUG ) # Install myself on top of pygame.font def install(): @@ -98,10 +62,7 @@ class PangoFont(object): if family is not None: fd.set_family(family) if size is not None: - log.debug( 'Pre-conversion size: %s', size ) - size = int(size*1024) - log.debug( 'Font size: %s', size, ) - fd.set_size(size) # XXX magic number, pango's scaling + fd.set_size(size*1000) self.fd = fd self.set_bold( bold ) self.set_italic( italic ) @@ -122,7 +83,17 @@ class PangoFont(object): """ log.info( 'render: %r, antialias = %s, color=%s, background=%s', text, antialias, color, background ) - layout = self._createLayout( text ) + # create layout + layout = pango.Layout(gtk.gdk.pango_context_get()) + layout.set_font_description(self.fd) + if self.underline: + attrs = layout.get_attributes() + if not attrs: + attrs = pango.AttrList() + attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767)) + layout.set_attributes( attrs ) + layout.set_text(text) + # determine pixel size (logical, ink) = layout.get_pixel_extents() ink = pygame.rect.Rect(ink) @@ -196,43 +167,8 @@ class PangoFont(object): """Set our current underlining properly""" self.underline = underline def get_underline( self ): - """Retrieve our current underline setting""" return self.underline - def _createLayout( self, text ): - """Produces a Pango layout describing this text in this font""" - # create layout - layout = pango.Layout(gtk.gdk.pango_context_get()) - layout.set_font_description(self.fd) - if self.underline: - attrs = layout.get_attributes() - if not attrs: - attrs = pango.AttrList() - attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767)) - layout.set_attributes( attrs ) - layout.set_text(text) - return layout - - def size( self, text ): - """Determine space required to render given text - - returns tuple of (width,height) - """ - layout = self._createLayout( text ) - (logical, ink) = layout.get_pixel_extents() - ink = pygame.rect.Rect(ink) - return (ink.width,ink.height) - -## def get_linesize( self ): -## """Determine inter-line spacing for the font""" -## font = self.get_context().load_font( self.fd ) -## metrics = font.get_metrics() -## return pango.PIXELS( metrics.get_ascent() ) -## def get_height( self ): -## def get_ascent( self ): -## def get_descent( self ): - - class SysFont(PangoFont): """Construct a PangoFont from a font description (name), size in pixels, bold, and italic designation. Similar to SysFont from Pygame.""" @@ -248,20 +184,17 @@ class SysFont(PangoFont): # originally defined a new class, no reason for that... NotImplemented = NotImplementedError +class Font(PangoFont): + """Abstract class, do not use""" + def __init__(self, *args, **kwargs): + raise NotImplementedError("PangoFont doesn't support Font directly, use SysFont or .fontByDesc") + def match_font(name,bold=False,italic=False): """Stub, does not work, use fontByDesc instead""" raise NotImplementedError("PangoFont doesn't support match_font directly, use SysFont or .fontByDesc") def fontByDesc(desc="",bold=False,italic=False): """Constructs a FontDescription from the given string representation. - -The format of the fontByDesc string representation is passed directly -to the pango.FontDescription constructor and documented at: - - http://www.pygtk.org/docs/pygtk/class-pangofontdescription.html#constructor-pangofontdescription - -Bold and italic are provided as a convenience. - The format of the string representation is: "[FAMILY-LIST] [STYLE-OPTIONS] [SIZE]" diff --git a/olpcgames/pausescreen.py b/olpcgames/pausescreen.py index 113a0ea..1c1d6c3 100755..100644 --- a/olpcgames/pausescreen.py +++ b/olpcgames/pausescreen.py @@ -7,39 +7,12 @@ we have more involved activities using the code. We use svgsprite to render a graphic which is stored in the olpcgames data directory over a dimmed version of the current screen contents. - -_LAST_EVENT_TIME -- tracks the last time that we saw an event - come across the wire. """ import logging log = logging.getLogger( 'olpcgames.pausescreen' ) import pygame from pygame import sprite -_LAST_EVENT_TIME = 0 - -def _set_last_event_time( time=None ): - """Set time as the last event time - - time -- if None, pygame.time.get_ticks() is used - - returns time set - """ - global _LAST_EVENT_TIME - if time is None: - time = pygame.time.get_ticks() - _LAST_EVENT_TIME = time - return time - -def last_event_time( ): - """Return the duration since last event for pausing operations - - returns time in seconds - """ - global _LAST_EVENT_TIME - return (pygame.time.get_ticks() - _LAST_EVENT_TIME)/1000. - - def get_events( sleep_timeout = 10, pause=None, **args ): """Retrieve the set of pending events or sleep @@ -48,7 +21,7 @@ def get_events( sleep_timeout = 10, pause=None, **args ): by taking the current screen and modifying it in some way. Defaults to pauseScreen in this module. If you return nothing from this function then no restoration or display-flipping will occur - *args -- if present, passed to 'pause' to configuration operation (e.g. + *args -- if present, passed to pause to configuration operation (e.g. to specify a different overlaySVG file) returns set of pending events (potentially empty) @@ -59,7 +32,7 @@ def get_events( sleep_timeout = 10, pause=None, **args ): if not events: log.info( 'No events in queue' ) old_screen = None - if last_event_time() > sleep_timeout: + if hasattr(pygame.event, 'last_event_time') and pygame.event.last_event_time() > sleep_timeout: # we've been waiting long enough, go to sleep visually log.warn( 'Pausing activity after %s with function %s', sleep_timeout, pause ) old_screen = pause( ) @@ -71,8 +44,8 @@ def get_events( sleep_timeout = 10, pause=None, **args ): log.warn( 'Activity restarted') if old_screen: restoreScreen( old_screen ) - if events: - _set_last_event_time() + else: + log.info( 'Not running under OLPCGames' ) return events def pauseScreen( overlaySVG=None ): diff --git a/olpcgames/svgsprite.py b/olpcgames/svgsprite.py index ad247dd..2c53178 100755..100644 --- a/olpcgames/svgsprite.py +++ b/olpcgames/svgsprite.py @@ -1,16 +1,10 @@ """RSVG/Cairo-based rendering of SVG into Pygame Images""" -from pygame import sprite, Rect +from pygame import sprite from olpcgames import _cairoimage +import cairo, rsvg 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. - """ + """Sprite class which renders SVG source-code as a Pygame image""" rect = image = None resolution = None def __init__( @@ -36,7 +30,7 @@ class SVGSprite( sprite.Sprite ): width,height = self.size else: width,height = None,None - self.image = self._render( width,height ).convert_alpha() + self.image = self._render( width,height ) rect = self.image.get_rect() if self.rect: rect.move( self.rect ) # should let something higher-level do that... @@ -44,7 +38,6 @@ class SVGSprite( sprite.Sprite ): def _render( self, width, height ): """Render our SVG to a Pygame image""" - import rsvg handle = rsvg.Handle( data = self.svg ) originalSize = (width,height) scale = 1.0 @@ -73,12 +66,4 @@ class SVGSprite( sprite.Sprite ): handle.render_cairo( ctx ) return _cairoimage.asImage( csrf ) return None - def copy( self ): - """Create a copy of this sprite without reloading the svg image""" - result = self.__class__( - size = self.size - ) - result.image = self.image - result.rect = Rect(self.rect) - result.resolution = self.resolution - return result + diff --git a/olpcgames/textsprite.py b/olpcgames/textsprite.py deleted file mode 100755 index 7663630..0000000 --- a/olpcgames/textsprite.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Simple Sprite sub-class that renders via a PangoFont""" -from pygame import sprite -from olpcgames import pangofont - -class TextSprite( sprite.Sprite ): - """Sprite with a simple text renderer""" - image = rect = text = color = background = None - def __init__( self, text=None, family=None, size=None, bold=False, italic=False, color=None, background=None ): - super( TextSprite, self ).__init__( ) - self.font = pangofont.PangoFont( family=family, size=size, bold=bold, italic=italic ) - self.set_color( color ) - self.set_background( background ) - self.set_text( text ) - def set_text( self, text ): - """Set our text string and render to a graphic""" - self.text = text - self.render( ) - def set_color( self, color =None): - """Set our rendering colour (default white)""" - self.color = color or (255,255,255) - self.render() - def set_background( self, color=None ): - """Set our background color, default transparent""" - self.background = color - self.render() - def render( self ): - """Render our image and rect (or None,None) - - After a render you will need to move the rect member to the - correct location on the screen. - """ - if self.text: - self.image = self.font.render( self.text, color = self.color, background = self.background ) - currentRect = self.rect - self.rect = self.image.get_rect() - if currentRect: - self.rect.center = currentRect.center - else: - self.rect = None - self.image = None diff --git a/olpcgames/util.py b/olpcgames/util.py index 49a23b0..f4ecbf0 100755..100644 --- a/olpcgames/util.py +++ b/olpcgames/util.py @@ -58,22 +58,11 @@ def get_traceback(error): 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 = cStringIO.StringIO() - try: - traceback.print_exc( limit=10, file = file ) - exception = file.getvalue() - finally: - file.close() - return exception + exception = str(error) + file = cStringIO.StringIO() + try: + traceback.print_exc( limit=10, file = file ) + exception = file.getvalue() + finally: + file.close() + return exception diff --git a/olpcgames/video.py b/olpcgames/video.py index 032aa13..0cf9ac9 100755..100644 --- a/olpcgames/video.py +++ b/olpcgames/video.py @@ -1,18 +1,11 @@ -"""Video widget for displaying a gstreamer pipe - -Note: currently this module is not all that elegant or useful, -we need a better recipe for using and working with Video -under OLPCGames. -""" +"""Video widget for displaying a gstreamer pipe""" import logging log = logging.getLogger( 'olpcgames.video' ) #log.setLevel( logging.INFO ) import os import signal import pygame -import weakref import olpcgames -from olpcgames import _gtkmain import pygtk pygtk.require('2.0') @@ -86,7 +79,6 @@ class PygameWidget( object ): window_id = int(os.environ['SDL_WINDOWID']) self.window_id = window_id self._imagesink = None - #self._holder = _gtkmain.Holder() def set_sink( self, sink ): """Set up our gst sink""" log.info( 'Setting sink: %s', sink ) @@ -157,7 +149,7 @@ if __name__ == "__main__": display.flip() pgw = PygameWidget( ) - p = Player( pgw, pipe_desc=Player.test_pipe_desc ) + p = Player( pgw ) p.play() clock = pygame.time.Clock() -- cgit v0.9.1