Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSin Nombre <sin@ubuntu.(none)>2010-05-28 20:45:29 (GMT)
committer Sin Nombre <sin@ubuntu.(none)>2010-05-28 20:45:29 (GMT)
commit9e93d1b9802385900b6f833f81f84c0ac50f91ef (patch)
treef2c16599bde31b31e5d44bae4a87e7e4093f3cab
parentac8cbb6691ba3de1c7c42f4362edbe11270f4506 (diff)
Beta version complete with state and capital quizzesHEADmaster
-rwxr-xr-xMANIFEST180
-rwxr-xr-xactivity/activity.info6
-rw-r--r--activity/conozco-mexico.svg81
-rw-r--r--activity/conozco-numeros.svg109
-rwxr-xr-xconozcomx.py676
-rw-r--r--credits/Thanks/default.html13
-rw-r--r--gui.theme/Vera.ttfbin0 -> 65932 bytes
-rw-r--r--gui.theme/box.down.pngbin0 -> 312 bytes
-rw-r--r--gui.theme/box.hover.pngbin0 -> 324 bytes
-rw-r--r--gui.theme/box.normal.pngbin0 -> 242 bytes
-rw-r--r--gui.theme/box.xcfbin0 -> 2004 bytes
-rw-r--r--gui.theme/button.down.pngbin0 -> 944 bytes
-rw-r--r--gui.theme/button.hover.pngbin0 -> 709 bytes
-rw-r--r--gui.theme/button.normal.pngbin0 -> 709 bytes
-rw-r--r--gui.theme/check.pngbin0 -> 205 bytes
-rw-r--r--gui.theme/checkbox.off.hover.tgabin0 -> 442 bytes
-rw-r--r--gui.theme/checkbox.off.normal.tgabin0 -> 320 bytes
-rw-r--r--gui.theme/checkbox.on.hover.tgabin0 -> 535 bytes
-rw-r--r--gui.theme/checkbox.on.normal.tgabin0 -> 500 bytes
-rw-r--r--gui.theme/config.txt291
-rw-r--r--gui.theme/config.txt~291
-rw-r--r--gui.theme/console.input.focus.pngbin0 -> 224 bytes
-rw-r--r--gui.theme/console.input.normal.pngbin0 -> 224 bytes
-rw-r--r--gui.theme/console.pngbin0 -> 224 bytes
-rw-r--r--gui.theme/desktop.pngbin0 -> 181 bytes
-rw-r--r--gui.theme/desktop.xcfbin0 -> 1975 bytes
-rw-r--r--gui.theme/dialog.bar.pngbin0 -> 315 bytes
-rw-r--r--gui.theme/dialog.close.down.tgabin0 -> 677 bytes
-rw-r--r--gui.theme/dialog.close.hover.tgabin0 -> 668 bytes
-rw-r--r--gui.theme/dialog.close.normal.tgabin0 -> 582 bytes
-rw-r--r--gui.theme/dialog.pngbin0 -> 322 bytes
-rw-r--r--gui.theme/dot.down.pngbin0 -> 549 bytes
-rw-r--r--gui.theme/dot.hover.pngbin0 -> 548 bytes
-rw-r--r--gui.theme/dot.normal.pngbin0 -> 366 bytes
-rw-r--r--gui.theme/dot.xcfbin0 -> 1930 bytes
-rw-r--r--gui.theme/down.pngbin0 -> 202 bytes
-rw-r--r--gui.theme/filebrowser.folder.pngbin0 -> 634 bytes
-rw-r--r--gui.theme/generate.py98
-rw-r--r--gui.theme/hslider.bar.hover.tgabin0 -> 776 bytes
-rw-r--r--gui.theme/hslider.bar.normal.tgabin0 -> 729 bytes
-rw-r--r--gui.theme/hslider.left.tgabin0 -> 372 bytes
-rw-r--r--gui.theme/hslider.right.tgabin0 -> 372 bytes
-rw-r--r--gui.theme/hslider.tgabin0 -> 1231 bytes
-rw-r--r--gui.theme/idot.normal.pngbin0 -> 408 bytes
-rw-r--r--gui.theme/input.focus.pngbin0 -> 181 bytes
-rw-r--r--gui.theme/input.normal.pngbin0 -> 190 bytes
-rw-r--r--gui.theme/left.pngbin0 -> 209 bytes
-rw-r--r--gui.theme/list.item.down.pngbin0 -> 189 bytes
-rw-r--r--gui.theme/list.item.hover.pngbin0 -> 172 bytes
-rw-r--r--gui.theme/list.item.normal.pngbin0 -> 172 bytes
-rw-r--r--gui.theme/list.pngbin0 -> 129 bytes
-rw-r--r--gui.theme/listitem.down.tgabin0 -> 2226 bytes
-rw-r--r--gui.theme/listitem.hover.tgabin0 -> 2226 bytes
-rw-r--r--gui.theme/listitem.normal.tgabin0 -> 2322 bytes
-rw-r--r--gui.theme/menu.down.tgabin0 -> 528 bytes
-rw-r--r--gui.theme/menu.hover.tgabin0 -> 441 bytes
-rw-r--r--gui.theme/menu.normal.tgabin0 -> 78 bytes
-rw-r--r--gui.theme/notes.txt8
-rw-r--r--gui.theme/out.tgabin0 -> 795 bytes
-rw-r--r--gui.theme/progressbar.bar.tgabin0 -> 563 bytes
-rw-r--r--gui.theme/progressbar.tgabin0 -> 412 bytes
-rw-r--r--gui.theme/radio.off.hover.tgabin0 -> 582 bytes
-rw-r--r--gui.theme/radio.off.normal.tgabin0 -> 537 bytes
-rw-r--r--gui.theme/radio.on.hover.tgabin0 -> 637 bytes
-rw-r--r--gui.theme/radio.on.normal.tgabin0 -> 646 bytes
-rw-r--r--gui.theme/radio.pngbin0 -> 197 bytes
-rw-r--r--gui.theme/rdot.down.pngbin0 -> 525 bytes
-rw-r--r--gui.theme/rdot.hover.pngbin0 -> 529 bytes
-rw-r--r--gui.theme/rdot.normal.pngbin0 -> 370 bytes
-rw-r--r--gui.theme/right.pngbin0 -> 206 bytes
-rw-r--r--gui.theme/sbox.normal.pngbin0 -> 232 bytes
-rw-r--r--gui.theme/scroller.slide.bar.hover.tgabin0 -> 342 bytes
-rw-r--r--gui.theme/scroller.slide.bar.normal.tgabin0 -> 342 bytes
-rw-r--r--gui.theme/scroller.slide.h.tgabin0 -> 1278 bytes
-rw-r--r--gui.theme/scroller.slide.v.tgabin0 -> 1278 bytes
-rw-r--r--gui.theme/select.arrow.down.tgabin0 -> 344 bytes
-rw-r--r--gui.theme/select.arrow.hover.tgabin0 -> 363 bytes
-rw-r--r--gui.theme/select.arrow.normal.tgabin0 -> 363 bytes
-rw-r--r--gui.theme/select.arrow.pngbin0 -> 156 bytes
-rw-r--r--gui.theme/select.option.hover.pngbin0 -> 190 bytes
-rw-r--r--gui.theme/select.option.normal.pngbin0 -> 172 bytes
-rw-r--r--gui.theme/select.options.pngbin0 -> 181 bytes
-rw-r--r--gui.theme/select.selected.down.tgabin0 -> 322 bytes
-rw-r--r--gui.theme/select.selected.hover.tgabin0 -> 338 bytes
-rw-r--r--gui.theme/select.selected.normal.tgabin0 -> 282 bytes
-rw-r--r--gui.theme/slider.bar.hover.tgabin0 -> 776 bytes
-rw-r--r--gui.theme/slider.bar.normal.tgabin0 -> 729 bytes
-rw-r--r--gui.theme/slider.tgabin0 -> 1231 bytes
-rw-r--r--gui.theme/tool.down.tgabin0 -> 528 bytes
-rw-r--r--gui.theme/tool.hover.tgabin0 -> 563 bytes
-rw-r--r--gui.theme/tool.normal.tgabin0 -> 441 bytes
-rw-r--r--gui.theme/up.pngbin0 -> 195 bytes
-rw-r--r--gui.theme/vbox.normal.pngbin0 -> 233 bytes
-rw-r--r--gui.theme/vdot.down.pngbin0 -> 540 bytes
-rw-r--r--gui.theme/vdot.hover.pngbin0 -> 527 bytes
-rw-r--r--gui.theme/vdot.normal.pngbin0 -> 354 bytes
-rw-r--r--gui.theme/vsbox.normal.pngbin0 -> 224 bytes
-rw-r--r--gui.theme/vslider.bar.hover.tgabin0 -> 1332 bytes
-rw-r--r--gui.theme/vslider.bar.normal.tgabin0 -> 1412 bytes
-rw-r--r--gui.theme/vslider.down.tgabin0 -> 864 bytes
-rw-r--r--gui.theme/vslider.tgabin0 -> 1231 bytes
-rw-r--r--gui.theme/vslider.up.tgabin0 -> 864 bytes
-rw-r--r--gui.theme/x.pngbin0 -> 199 bytes
-rw-r--r--mxtheme.py25
-rwxr-xr-xolpcgames/COPYING24
-rwxr-xr-xolpcgames/__init__.py102
-rwxr-xr-xolpcgames/_cairoimage.py135
-rwxr-xr-xolpcgames/_gtkmain.py70
-rwxr-xr-xolpcgames/_version.py2
-rwxr-xr-xolpcgames/activity.py241
-rwxr-xr-xolpcgames/buildmanifest.py33
-rwxr-xr-xolpcgames/camera.py221
-rwxr-xr-xolpcgames/canvas.py171
-rwxr-xr-xolpcgames/data/__init__.py36
-rwxr-xr-xolpcgames/data/sleeping_svg.py61
-rwxr-xr-xolpcgames/dbusproxy.py93
-rwxr-xr-xolpcgames/eventwrap.py388
-rwxr-xr-xolpcgames/gtkEvent.py289
-rwxr-xr-xolpcgames/mesh.py583
-rwxr-xr-xolpcgames/pangofont.py346
-rwxr-xr-xolpcgames/pausescreen.py116
-rwxr-xr-xolpcgames/svgsprite.py84
-rwxr-xr-xolpcgames/textsprite.py40
-rwxr-xr-xolpcgames/util.py79
-rwxr-xr-xolpcgames/video.py178
-rw-r--r--pgu/LICENSE.txt504
-rw-r--r--pgu/__init__.py7
-rw-r--r--pgu/algo.py135
-rw-r--r--pgu/ani.py90
-rw-r--r--pgu/engine.py154
-rw-r--r--pgu/fonts.py130
-rw-r--r--pgu/gui/__init__.py32
-rw-r--r--pgu/gui/app.py226
-rw-r--r--pgu/gui/area.py435
-rw-r--r--pgu/gui/basic.py110
-rw-r--r--pgu/gui/button.py351
-rw-r--r--pgu/gui/const.py45
-rw-r--r--pgu/gui/container.py452
-rw-r--r--pgu/gui/deprecated.py76
-rw-r--r--pgu/gui/dialog.py156
-rw-r--r--pgu/gui/document.py112
-rw-r--r--pgu/gui/form.py79
-rw-r--r--pgu/gui/group.py43
-rw-r--r--pgu/gui/input.py166
-rw-r--r--pgu/gui/keysym.py72
-rw-r--r--pgu/gui/layout.py172
-rw-r--r--pgu/gui/menus.py121
-rw-r--r--pgu/gui/misc.py43
-rw-r--r--pgu/gui/select.py191
-rw-r--r--pgu/gui/slider.py281
-rw-r--r--pgu/gui/style.py42
-rw-r--r--pgu/gui/surface.py135
-rw-r--r--pgu/gui/table.py331
-rw-r--r--pgu/gui/textarea.py290
-rw-r--r--pgu/gui/theme.py473
-rw-r--r--pgu/gui/widget.py319
-rw-r--r--pgu/hexvid.py127
-rw-r--r--pgu/high.py154
-rw-r--r--pgu/html.py536
-rw-r--r--pgu/isovid.py182
-rw-r--r--pgu/layout.py4
-rw-r--r--pgu/text.py61
-rw-r--r--pgu/tilevid.py195
-rw-r--r--pgu/timer.py68
-rw-r--r--pgu/vid.py560
-rw-r--r--po/ConozcoNumeros.pot58
-rwxr-xr-xrecursos/comun/datos/creditos.txt6
-rw-r--r--recursos/comun/datos/creditos.txt~1
-rw-r--r--recursos/lamina/LICENSE10
-rwxr-xr-xrecursos/lamina/bicho.pngbin0 -> 13333 bytes
-rw-r--r--recursos/lamina/capital_icon.pngbin0 -> 2832 bytes
-rw-r--r--recursos/lamina/estado_icon.xcfbin0 -> 12042 bytes
-rwxr-xr-xrecursos/lamina/globito.pngbin0 -> 1583 bytes
-rw-r--r--recursos/lamina/layout.pngbin0 -> 30805 bytes
-rw-r--r--recursos/lamina/layout.png.origbin0 -> 39969 bytes
-rw-r--r--recursos/lamina/layout_cmx.pngbin0 -> 9266 bytes
-rw-r--r--recursos/lamina/levels_capitals.txt59
-rw-r--r--recursos/lamina/levels_states.txt59
-rw-r--r--recursos/lamina/mexico.pngbin0 -> 28582 bytes
-rw-r--r--recursos/lamina/mexico_capitals_long.pngbin0 -> 66114 bytes
-rw-r--r--recursos/lamina/mexico_estados.xcfbin273187 -> 360774 bytes
-rw-r--r--recursos/lamina/mexico_estados_long.xcfbin0 -> 288433 bytes
-rw-r--r--recursos/lamina/mexico_estados_scaled.pngbin0 -> 53841 bytes
-rw-r--r--recursos/lamina/mexico_estados_scaled.xcfbin0 -> 261925 bytes
-rw-r--r--recursos/lamina/mexico_estados_solo.pngbin63093 -> 59122 bytes
-rw-r--r--recursos/lamina/mexico_states_long.pngbin0 -> 68180 bytes
-rw-r--r--recursos/lamina/niveles.txt.backup (renamed from recursos/lamina/niveles.txt)0
-rw-r--r--recursos/lamina/niveles.txt~181
-rw-r--r--recursos/lamina/state_icon.pngbin0 -> 3468 bytes
-rw-r--r--recursos/lamina/terron.pngbin0 -> 11964 bytes
-rw-r--r--recursos/lamina/zona_definiciones.txt~35
-rw-r--r--recursos/lamina/zonas.png.backup (renamed from recursos/lamina/zonas.png)bin30670 -> 30670 bytes
-rw-r--r--recursos/lamina/zonas.txt.backup (renamed from recursos/lamina/zonas.txt)0
-rw-r--r--recursos/lamina/zonas.txt~32
-rw-r--r--recursos/lamina/zone_definitiones_capitals.txt33
-rw-r--r--recursos/lamina/zone_definitiones_states.txt35
-rw-r--r--recursos/lamina/zones_capitals.pngbin0 -> 55290 bytes
-rw-r--r--recursos/lamina/zones_capitals.txt32
-rw-r--r--recursos/lamina/zones_scaled.pngbin0 -> 37353 bytes
-rw-r--r--recursos/lamina/zones_states.pngbin0 -> 66525 bytes
-rw-r--r--recursos/lamina/zones_states.txt32
-rw-r--r--recursos/lamina/zones_states_bad.pngbin0 -> 14792 bytes
202 files changed, 12877 insertions, 426 deletions
diff --git a/MANIFEST b/MANIFEST
index b6ffa17..4a6dc3a 100755
--- a/MANIFEST
+++ b/MANIFEST
@@ -2,9 +2,9 @@ conozcomx.py
run.py
activity.py
setup.py
-NEWS
-COPYING
-activity/conozco-numeros.svg
+
+
+
activity/activity.info
olpcgames/COPYING
olpcgames/_cairoimage.py
@@ -27,15 +27,165 @@ olpcgames/buildmanifest.py
olpcgames/dbusproxy.py
olpcgames/data/__init__.py
olpcgames/data/sleeping_svg.py
-recursos/comun/datos/creditos.txt
-recursos/comun/fuentes/AllCaps.ttf
-recursos/comun/fuentes/Share-Regular.ttf
-recursos/comun/imagenes/globito.png
-recursos/comun/imagenes/bicho.png
-recursos/comun/imagenes/terron.png
-recursos/comun/imagenes/LICENSE
-recursos/comun/sonidos/junggle_btn045.wav
-recursos/lamina/niveles.txt
-recursos/lamina/zonas.txt
-recursos/lamina/zonas.png
-recursos/lamina/lamina.png
+
+mxtheme.py
+
+recursos/lamina/niveles.txt.backup
+recursos/lamina/LICENSE
+recursos/lamina/layout_cmx.png
+
+
+recursos/comun/datos/presentacion.txt
+pgu/algo.py
+pgu/__init__.py
+pgu/text.py
+pgu/vid.py
+pgu/high.py
+pgu/ani.py
+pgu/fonts.py
+pgu/LICENSE.txt
+pgu/html.py
+pgu/isovid.py
+pgu/hexvid.py
+pgu/engine.py
+pgu/timer.py
+pgu/layout.py
+pgu/tilevid.py
+pgu/gui/widget.py
+pgu/gui/style.py
+pgu/gui/__init__.py
+pgu/gui/dialog.py
+pgu/gui/group.py
+pgu/gui/area.py
+pgu/gui/input.py
+pgu/gui/slider.py
+pgu/gui/surface.py
+pgu/gui/theme.py
+pgu/gui/menus.py
+pgu/gui/deprecated.py
+pgu/gui/button.py
+pgu/gui/const.py
+pgu/gui/table.py
+pgu/gui/app.py
+pgu/gui/textarea.py
+pgu/gui/document.py
+pgu/gui/basic.py
+pgu/gui/misc.py
+pgu/gui/container.py
+pgu/gui/form.py
+pgu/gui/layout.py
+pgu/gui/keysym.py
+pgu/gui/select.py
+gui.theme/desktop.xcf
+gui.theme/hslider.tga
+gui.theme/dialog.close.normal.tga
+gui.theme/select.option.hover.png
+gui.theme/select.arrow.normal.tga
+gui.theme/vslider.bar.hover.tga
+gui.theme/vdot.down.png
+gui.theme/vslider.up.tga
+gui.theme/generate.py
+gui.theme/select.arrow.down.tga
+gui.theme/desktop.png
+gui.theme/box.xcf
+gui.theme/x.png
+gui.theme/console.input.normal.png
+gui.theme/tool.normal.tga
+gui.theme/notes.txt
+gui.theme/vslider.tga
+gui.theme/tool.hover.tga
+gui.theme/rdot.down.png
+gui.theme/config.txt
+gui.theme/checkbox.off.hover.tga
+gui.theme/list.item.hover.png
+gui.theme/down.png
+gui.theme/left.png
+gui.theme/radio.off.normal.tga
+gui.theme/list.item.down.png
+gui.theme/scroller.slide.bar.hover.tga
+gui.theme/listitem.down.tga
+gui.theme/right.png
+gui.theme/input.focus.png
+gui.theme/box.down.png
+gui.theme/radio.off.hover.tga
+gui.theme/dialog.png
+gui.theme/dot.hover.png
+gui.theme/box.hover.png
+gui.theme/menu.normal.tga
+gui.theme/vbox.normal.png
+gui.theme/menu.hover.tga
+gui.theme/scroller.slide.v.tga
+gui.theme/slider.tga
+gui.theme/select.selected.hover.tga
+gui.theme/hslider.bar.hover.tga
+gui.theme/box.normal.png
+gui.theme/dialog.bar.png
+gui.theme/tool.down.tga
+gui.theme/radio.png
+gui.theme/idot.normal.png
+gui.theme/console.png
+gui.theme/select.arrow.png
+gui.theme/progressbar.tga
+gui.theme/select.selected.down.tga
+gui.theme/up.png
+gui.theme/filebrowser.folder.png
+gui.theme/select.arrow.hover.tga
+gui.theme/radio.on.normal.tga
+gui.theme/slider.bar.hover.tga
+gui.theme/checkbox.on.normal.tga
+gui.theme/checkbox.off.normal.tga
+gui.theme/vdot.normal.png
+gui.theme/checkbox.on.hover.tga
+gui.theme/hslider.bar.normal.tga
+gui.theme/dot.normal.png
+gui.theme/vslider.down.tga
+gui.theme/vsbox.normal.png
+gui.theme/list.item.normal.png
+gui.theme/input.normal.png
+gui.theme/dialog.close.down.tga
+gui.theme/vslider.bar.normal.tga
+gui.theme/button.normal.png
+gui.theme/button.hover.png
+gui.theme/out.tga
+gui.theme/select.options.png
+gui.theme/hslider.right.tga
+gui.theme/hslider.left.tga
+gui.theme/select.option.normal.png
+gui.theme/list.png
+gui.theme/dot.xcf
+gui.theme/scroller.slide.bar.normal.tga
+gui.theme/vdot.hover.png
+gui.theme/listitem.normal.tga
+gui.theme/check.png
+gui.theme/sbox.normal.png
+gui.theme/scroller.slide.h.tga
+gui.theme/console.input.focus.png
+gui.theme/dot.down.png
+gui.theme/listitem.hover.tga
+gui.theme/rdot.hover.png
+gui.theme/select.selected.normal.tga
+gui.theme/progressbar.bar.tga
+gui.theme/button.down.png
+gui.theme/rdot.normal.png
+gui.theme/Vera.ttf
+gui.theme/dialog.close.hover.tga
+gui.theme/radio.on.hover.tga
+gui.theme/menu.down.tga
+gui.theme/slider.bar.normal.tga
+po/ConozcoNumeros.pot
+recursos/lamina/zones_capitals.txt
+recursos/lamina/zones_capitals.png
+recursos/lamina/levels_states.txt
+recursos/lamina/state_icon.png
+recursos/lamina/levels_capitals.txt
+recursos/lamina/zones_states.txt
+recursos/lamina/zones_states.png
+recursos/lamina/capital_icon.png
+recursos/lamina/mexico_states_long.png
+recursos/lamina/mexico_capitals_long.png
+recursos/lamina/zone_definitiones_states.txt
+recursos/lamina/mexico.png
+recursos/lamina/zone_definitiones_capitals.txt
+recursos/lamina/zones_states_bad.png
+activity/conozco-mexico.svg
+credits/credit1/default.html
diff --git a/activity/activity.info b/activity/activity.info
index d24a302..711d456 100755
--- a/activity/activity.info
+++ b/activity/activity.info
@@ -1,8 +1,8 @@
[Activity]
-name = Conozco Numeros
+name = Conozco Mexico
activity_version = 1
host_version = 1
-bundle_id = org.ceibaljam.conozconumeros
-icon = conozco-numeros
+bundle_id = org.sugarlabs.git.projects.conozcomexico
+icon = conozco-mexico
exec = sugar-activity activity.Activity
license = GPL+
diff --git a/activity/conozco-mexico.svg b/activity/conozco-mexico.svg
new file mode 100644
index 0000000..84ec327
--- /dev/null
+++ b/activity/conozco-mexico.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="55"
+ height="55"
+ id="svg4502"
+ style="display:inline"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="conozco-mexico.svg">
+ <metadata
+ id="metadata2876">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1274"
+ inkscape:window-height="772"
+ id="namedview2874"
+ showgrid="false"
+ inkscape:zoom="8.5818182"
+ inkscape:cx="37.057362"
+ inkscape:cy="27.959611"
+ inkscape:window-x="0"
+ inkscape:window-y="1"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4502" />
+ <defs
+ id="defs4504">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective2878" />
+ <inkscape:perspective
+ id="perspective2944"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3015"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <image
+ y="9.4491539"
+ x="-0.047580637"
+ id="image3017"
+ height="36.567795"
+ width="55.069172"
+ xlink:href="file:///home/sin/olpc/conozco-mexico/mainline/recursos/lamina/mexico.png" />
+</svg>
diff --git a/activity/conozco-numeros.svg b/activity/conozco-numeros.svg
deleted file mode 100644
index 4c2fd79..0000000
--- a/activity/conozco-numeros.svg
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- version="1.0"
- width="55"
- height="55"
- id="svg4502"
- style="display:inline"
- inkscape:version="0.47pre4 r22446"
- sodipodi:docname="conozco-numeros.svg">
- <metadata
- id="metadata2876">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <sodipodi:namedview
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1"
- objecttolerance="10"
- gridtolerance="10"
- guidetolerance="10"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:window-width="1278"
- inkscape:window-height="780"
- id="namedview2874"
- showgrid="false"
- inkscape:zoom="4.2909091"
- inkscape:cx="27.5"
- inkscape:cy="27.5"
- inkscape:window-x="0"
- inkscape:window-y="18"
- inkscape:window-maximized="0"
- inkscape:current-layer="svg4502" />
- <defs
- id="defs4504">
- <inkscape:perspective
- sodipodi:type="inkscape:persp3d"
- inkscape:vp_x="0 : 27.5 : 1"
- inkscape:vp_y="0 : 1000 : 0"
- inkscape:vp_z="55 : 27.5 : 1"
- inkscape:persp3d-origin="27.5 : 18.333333 : 1"
- id="perspective2878" />
- </defs>
- <g
- id="terron"
- transform="matrix(0.5,0,0,0.5,23.934584,23.639858)">
- <path
- id="cuerpo"
- d="m 38.822128,44.108722 c 0,0 -0.376363,0 -0.729309,0 0,-1.361344 0.0042,-5.649084 0.0042,-6.783539 0,0 0,0 0,-0.0017 l 0,-12.760539 -0.0075,-7.751552 c -0.0033,-0.878581 -0.727637,-1.586579 -1.609164,-1.582439 -0.427382,0 -0.831345,0.16727 -1.132437,0.467859 -0.301927,0.302245 -0.465854,0.702202 -0.464182,1.127001 0,0.03975 0.0017,2.534719 0.0042,4.633873 l -12.816435,0 c -1.740473,0 -3.151419,1.396952 -3.151419,3.117679 l 0,1.724867 -5.654655,0.0033 c -0.887382,0.0058 -1.601636,0.72042 -1.59829,1.593205 0.0017,0.424799 0.170617,0.823099 0.474217,1.121205 0.305274,0.298933 0.709237,0.461234 1.135782,0.459578 0.04935,0 3.419892,-0.0017 5.642109,-0.0033 l 0,7.540395 c 0,0.01159 -0.0033,0.02153 -0.0033,0.03229 0,0.05797 0.0033,5.498377 0.0033,7.060942 -0.352109,0 -0.729309,0 -0.729309,0 -0.904108,0.01739 -1.605818,0.74195 -1.590763,1.616391 0.0184,0.876924 0.752727,1.572502 1.635091,1.555112 l 2.303345,-0.0017 c 0.9016,-0.01656 1.607491,-0.741949 1.589091,-1.616389 l -0.0033,-5.219318 12.764581,0 -0.0059,5.219318 c -0.01673,0.875268 0.689163,1.600657 1.590764,1.616389 l 2.301672,0.0017 c 0.886545,0.01739 1.619201,-0.678188 1.636764,-1.555112 0.01756,-0.874441 -0.687491,-1.598173 -1.589091,-1.615563 z M 30.604855,25.673428 c 0.592982,-0.08281 1.143309,0.326259 1.227782,0.914187 0.08531,0.587101 -0.329527,1.132798 -0.922509,1.217261 -0.596328,0.08281 -1.145818,-0.327914 -1.229455,-0.914187 -0.08531,-0.588757 0.329528,-1.134454 0.924182,-1.217261 z m -5.654654,0.794117 c 0.592145,-0.08281 1.143309,0.32626 1.226946,0.914189 0.08531,0.588756 -0.329528,1.131969 -0.921674,1.217261 -0.596327,0.08446 -1.145817,-0.32626 -1.229454,-0.914188 -0.08447,-0.588758 0.327855,-1.132798 0.924182,-1.217262 z m 1.434364,9.080603 c -1.020364,-0.72042 -2.6404,-2.733455 -2.058292,-4.100596 0.205746,-0.481109 0.658219,-0.182176 1.150837,0.0621 1.175927,0.594553 2.576,0.670735 3.831382,0.29562 0.638145,-0.190456 1.183454,-0.499326 1.703673,-0.911703 0.316144,-0.252562 1.179272,-1.257009 1.654326,-0.803228 0.371346,0.355242 0.43491,0.795775 0.43491,0.799915 0.532763,3.368583 -3.68251,6.798445 -6.716836,4.657887 z"
- style="fill:#666666" />
- <path
- id="pelo_oscuro"
- d="m 17.233073,11.642631 c 1.738801,-2.9454406 7.191892,-2.9669703 8.1788,0.590413 0.3312,1.195731 0.305274,3.767713 -1.020363,4.464947 -0.465855,0.245108 -0.627273,-0.269951 -0.851418,-0.768448 -0.536946,-1.193247 -1.570691,-2.129793 -2.784255,-2.622494 -0.6164,-0.251732 -1.236146,-0.356897 -1.901891,-0.36021 -0.407309,-0.0033 -1.716218,0.237656 -1.793163,-0.414034 -0.06022,-0.505122 0.17229,-0.888518 0.17229,-0.890174 z"
- style="fill:#666666" />
- <g
- transform="matrix(0.8363636,0,0,0.8280687,4.3773279,7.643887)"
- id="pelo_claro">
- <path
- d="M 30.514,13.666 C 25.959,9.955 27.36,0.759 33.675,0.043 c 2.124,-0.241 6.496,0.494 7.331,2.911 0.293,0.848 -0.624,0.982 -1.533,1.226 -2.174,0.585 -4.039,2.081 -5.2,3.992 -0.592,0.972 -0.931,1.989 -1.113,3.109 -0.113,0.683 -0.052,2.955 -1.178,2.913 -0.875,-0.032 -1.466,-0.526 -1.468,-0.528 z"
- id="path2674"
- style="fill:#666666" />
- </g>
- </g>
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
- x="7.4576273"
- y="20.508474"
- id="text2880"><tspan
- sodipodi:role="line"
- id="tspan2882"
- x="7.4576273"
- y="20.508474">27</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
- x="10.254237"
- y="38.68644"
- id="text2884"><tspan
- sodipodi:role="line"
- id="tspan2886"
- x="10.254237"
- y="38.68644">85</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:14px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
- x="28.432203"
- y="24.470337"
- id="text2888"><tspan
- sodipodi:role="line"
- id="tspan2890"
- x="28.432203"
- y="24.470337">48</tspan></text>
-</svg>
diff --git a/conozcomx.py b/conozcomx.py
index 3044e1e..5f297fb 100755
--- a/conozcomx.py
+++ b/conozcomx.py
@@ -1,6 +1,6 @@
#! /usr/bin/env python
-# Conozco
-# Copyright (C) 2010 Gabriel Eirea
+# Conozco Mexico
+# Copyright (C) 2010
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
-# Based off of Conozco - Numeros / Uruguay
+# Based off of Conozco - Numeros / Uruguay, Storybuilder
# Contact information:
# Cole cshaw@cidesi.mx
@@ -26,27 +26,36 @@ import os
import pygame
import olpcgames
import gtk
+import pickle
+import time
+import logging
+import pgu
+import pygame
+
+from pygame.locals import *
+
from gettext import gettext as _
+from datetime import date
+from olpcgames import eventwrap
+from sugar.graphics import style
+
+from pgu import text
+from pgu import gui
+from pgu import html
+
+#from storytheme import theme_list, theme_defs
+from mxtheme import theme_list, theme_defs
# constantes
XMAPAMAX = 786
DXPANEL = 414
XCENTROPANEL = 1002
-YGLOBITO = 310
-DXBICHO = 218
-DYBICHO = 268
-XBICHO = 1200-DXBICHO
-YBICHO = 900-DYBICHO
+
CAMINORECURSOS = "recursos"
CAMINOLAMINA = "lamina"
CAMINOCOMUN = "comun"
CAMINOFUENTES = "fuentes"
-ARCHIVOINFO = "info.txt"
CAMINODATOS = "datos"
-ARCHIVONIVELES = "niveles.txt"
-ARCHIVOZONAS = "zonas.txt"
-ARCHIVOCREDITOS = "creditos.txt"
-ARCHIVONOMBRE = "nombre.txt"
CAMINOIMAGENES = "imagenes"
CAMINOSONIDOS = "sonidos"
COLORNOMBREDEPTO = (200,60,60)
@@ -57,9 +66,40 @@ EVENTORESPUESTA = pygame.USEREVENT+1
TIEMPORESPUESTA = 2300
EVENTOREFRESCO = EVENTORESPUESTA+1
TIEMPOREFRESCO = 250
+FPS = 30
# variables globales para adaptar la pantalla a distintas resoluciones
-scale = 1
+ORIG_WIDTH = 1024
+ORIG_HEIGHT = 700
+
+
+FACTOR = min((gtk.gdk.screen_width() / float(ORIG_WIDTH)),
+ (gtk.gdk.screen_height()-style.MEDIUM_ICON_SIZE) / float(ORIG_HEIGHT))
+
+def scale(x):
+ if isinstance(x, int):
+ return int(x * FACTOR)
+ return tuple([scale(i) for i in x])
+
+GAME_WIDTH = scale(ORIG_WIDTH)
+GAME_HEIGHT = scale(ORIG_HEIGHT)
+
+CHAR_PANEL_SIZE = 5
+
+# button positions
+CLEAR = pygame.rect.Rect(scale((932, 435, 46, 33)))
+EXIT_GAME = pygame.rect.Rect(scale((46, 93, 46, 33)))
+ABOUT_GAME = pygame.rect.Rect(scale((46, 491, 46, 33)))
+PLAY_LEVEL_1 = pygame.rect.Rect(scale((932, 435, 46, 33)))
+QUIT_LEVEL = pygame.rect.Rect(scale((932, 114, 46, 33)))
+THEME_LEFT = pygame.rect.Rect(scale((410, 506, 46, 33)))
+THEME_RIGHT = pygame.rect.Rect(scale((567, 506, 46, 33)))
+#TEXTAREA = pygame.rect.Rect(scale((62, 560, 875, 114)))
+TEXTAREA = pygame.rect.Rect(scale((62, 560, 875, 114)))
+GAMEHEADING = pygame.rect.Rect(scale((300, 200, 50, 100)))
+BACKGROUNDAREA = pygame.rect.Rect(scale((153, 95, 754, 393)))
+
+#scale = 1
shift_x = 0
shift_y = 0
xo_resolution = True
@@ -72,27 +112,125 @@ def wait_events():
clock.tick(20)
return pygame.event.get()
+class Theme:
+ """Model for a theme."""
+ def __init__(self, themename):
+ """Create the theme based on the name.
+
+ themename -- string.
+ """
+ self.themename = themename
+ self.background = theme_defs[themename]['background']
+ self.buttons = theme_defs[themename]['buttons']
+ self.icon = theme_defs[themename]['icon']
+ self.zones = theme_defs[themename]['zones']
+ self.levels = theme_defs[themename]['levels']
+ self.zonepic = theme_defs[themename]['zonepic']
+
+
+def cargarImagen(nombre, (x, y)=(None, None)):
+ """Carga una imagen y la escala de acuerdo a la resolucion"""
+ fullname = os.path.join(CAMINORECURSOS, CAMINOLAMINA, nombre)
+ imagen0 = pygame.image.load(fullname).convert_alpha()
+ imagen = pygame.transform.scale(imagen0,
+ (scale(imagen0.get_width()),
+ scale(imagen0.get_height())))
+ if x and y:
+ imagen = pygame.transform.scale(imagen0, (scale(x), scale(y)))
+ return imagen, imagen.get_rect()
+
+class Icon(pygame.sprite.Sprite):
+ """Icon for Theme."""
+
+ def __init__(self, theme, layout=None):
+ """Load the icon.
+
+ theme -- Theme object
+ layout -- optional sprite to align with
+ """
+ pygame.sprite.Sprite.__init__(self)
+ self.image, self.rect = cargarImagen(theme.icon, (100, 66))
+ if layout:
+ x = layout.rect.left + scale(462)
+ y = layout.rect.top + scale(492)
+ self.rect.topleft = (x, y)
+
+class Layout(pygame.sprite.Sprite):
+ """Image of the screen layout."""
+
+ def __init__(self, name, surfsize=None):
+ """Create a Layout.
+
+ Parameters:
+ name: (string) filename of background image, from the data/ dir
+ surfsize: (optional tuple) size of surface to center on
+ """
+ pygame.sprite.Sprite.__init__(self)
+ self.image, self.rect = cargarImagen(name, (ORIG_WIDTH, ORIG_HEIGHT))
+ if surfsize:
+ mid_x = surfsize[0] / 2
+ mid_y = surfsize[1] / 2
+ self.rect.center = (mid_x, mid_y)
+
+ @property
+ def x(self):
+ return self.rect.top
+
+class Background(pygame.sprite.Sprite):
+ """Background image for Story."""
+
+ def __init__(self, theme, layout):
+ """Load the background.
+
+ theme -- Theme object
+ layout -- sprite to align with
+ """
+ pygame.sprite.Sprite.__init__(self)
+ self.image, self.rect = cargarImagen(theme.background, (754, 393))
+ x = layout.rect.left + scale(153)
+ y = layout.rect.top + scale(95)
+ self.rect.topleft = (x, y)
+
+class Widget:
+ """A button that controls something."""
+
+ def __init__(self, rect, layout, click_method):
+ """Create the Widget"""
+ self.rect = pygame.rect.Rect(rect)
+ x = layout.rect.left + rect.left
+ y = layout.rect.top + rect.top
+ self.rect.topleft = (x, y)
+ self.click_method = click_method
+
+ def is_mouse_over(self):
+ """Check if the mouse pointer is over the sprite."""
+ return self.rect.collidepoint(pygame.mouse.get_pos())
+
+ def click(self):
+ self.click_method()
+
+
class Zona():
"""Clase para zonas de una imagen.
La posicion esta dada por una imagen bitmap pintada con un color
especifico, dado por la clave (valor 0 a 255 del componente rojo).
+ En multiples de 5 (0, 5, 10, 15, etc.)
"""
- def __init__(self,mapa,nombre,claveColor,tipo,posicion,rotacion):
+ def __init__(self,mapa,nombre,claveColor,tipo):
self.mapa = mapa
self.nombre = nombre
self.claveColor = int(claveColor)
self.tipo = int(tipo)
- self.posicion = (int(int(posicion[0])*scale+shift_x),
- int(int(posicion[1])*scale+shift_y))
- self.rotacion = int(rotacion)
def estaAca(self,pos):
"""Devuelve True si la coordenada pos esta en la zona"""
- if pos[0] < XMAPAMAX*scale+shift_x:
- colorAca = self.mapa.get_at((pos[0]-shift_x, pos[1]-shift_y))
+ if BACKGROUNDAREA.collidepoint(pos):
+ newx = pos[0] - (BACKGROUNDAREA.topleft[0])
+ newy = pos[1] - (BACKGROUNDAREA.topleft[1])
+ colorAca = self.mapa.get_at((newx, newy))
if colorAca[0] == self.claveColor:
return True
else:
@@ -176,20 +314,20 @@ class ConozcoMx():
def cargarZonas(self):
"""Carga las imagenes y los datos de las zonas"""
- self.zonas = self.cargarImagen("zonas.png")
+ self.zonas = cargarImagen(self.theme.zonepic)[0]
self.listaZonas = list()
# falta sanitizar manejo de archivo
- f = open(os.path.join(self.camino_datos,ARCHIVOZONAS),"r")
+ f = open(os.path.join(self.camino_datos,self.theme.zones),"r")
linea = f.readline()
while linea:
if linea[0] == "#":
linea = f.readline()
continue
- [nombreZona,claveColor,posx,posy,rotacion] = \
+ [nombreZona,claveColor] = \
linea.strip().split("|")
nuevaZona = Zona(self.zonas,
unicode(nombreZona,'iso-8859-1'),
- claveColor,1,(posx,posy),rotacion)
+ claveColor,1)
self.listaZonas.append(nuevaZona)
linea = f.readline()
f.close()
@@ -203,7 +341,7 @@ class ConozcoMx():
self.listaMal = list()
self.listaDespedidas = list()
# falta sanitizar manejo de archivo
- f = open(os.path.join(self.camino_datos,ARCHIVONIVELES),"r")
+ f = open(os.path.join(self.camino_datos,self.theme.levels),"r")
linea = f.readline()
while linea:
if linea[0] == "#":
@@ -251,259 +389,92 @@ class ConozcoMx():
self.numeroMal = len(self.listaMal)
self.numeroDespedidas = len(self.listaDespedidas)
- def pantallaAcercaDe(self):
- """Pantalla con los datos del juego, creditos, etc"""
- global scale, shift_x, shift_y, xo_resolution
- self.pantallaTemp = pygame.Surface(
- (self.anchoPantalla,self.altoPantalla))
- self.pantallaTemp.blit(self.pantalla,(0,0))
- self.pantalla.fill((0,0,0))
- self.pantalla.blit(self.terron,
- (int(20*scale+shift_x),
- int(20*scale+shift_y)))
- self.mostrarTexto(_("Acerca de Conozco Mexico"),
- self.fuente40,
- (int(600*scale+shift_x),
- int(100*scale+shift_y)),
- (255,255,255))
- # falta sanitizar acceso a archivo
- f = open(os.path.join(CAMINORECURSOS,
- CAMINOCOMUN,
- CAMINODATOS,
- ARCHIVOCREDITOS),"r")
- yLinea = int(200*scale+shift_y)
- for linea in f:
- self.mostrarTexto(linea.strip(),
- self.fuente32,
- (int(600*scale+shift_x),yLinea),
- (155,155,255))
- yLinea = yLinea + int(40*scale)
- f.close()
- self.mostrarTexto(_("Presiona cualquier tecla para volver"),
- self.fuente32,
- (int(600*scale+shift_x),
- int(800*scale+shift_y)),
- (255,155,155))
- pygame.display.flip()
- while 1:
- for event in wait_events():
- if event.type == pygame.KEYDOWN:
- self.click.play()
- self.pantalla.blit(self.pantallaTemp,(0,0))
- pygame.display.flip()
- return
- elif event.type == EVENTOREFRESCO:
- pygame.display.flip()
+ def __init__(self):
+ """Esta es la inicializacion del juego"""
+ pygame.init()
+ self.screen = pygame.display.set_mode((GAME_WIDTH, GAME_HEIGHT))
+ pygame.display.set_caption(_("Conozco Mexico"))
+
+ self.canvas = pygame.Surface(self.screen.get_size()).convert()
+ self.canvas.fill((0xAC, 0xAC, 0xAC))
+
+ # pick the first theme as the default
+ self._themes = load_themes()
+ self.current_theme = 0
+ self.layout = Layout('layout_cmx.png')
+ self.set_theme(True)
+ self.text = ''
- def pantallaInicial(self):
- """Pantalla con el menu principal del juego"""
- global scale, shift_x, shift_y
- self.pantalla.fill((0,0,0))
- self.mostrarTexto(_("CONOZCO MEXICO"),
- self.fuente48,
- (int(600*scale+shift_x),
- int(80*scale+shift_y)),
- (255,255,255))
- self.mostrarTexto(_("Juego"),
- self.fuente48,
- (int(300*scale+shift_x), int(220*scale+shift_y)),
- (200,100,100))
- yLista = int(300*scale+shift_y)
- for n in self.listaNiveles:
- self.pantalla.fill((20,20,20),
- (int(10*scale+shift_x),
- yLista-int(24*scale),
- int(590*scale),
- int(48*scale)))
- self.mostrarTexto(n.nombre,
- self.fuente40,
- (int(300*scale+shift_x), yLista),
- (200,100,100))
- yLista += int(50*scale)
- self.pantalla.fill((20,20,20),
- (int(10*scale+shift_x),
- int(801*scale+shift_y),
- int(590*scale),int(48*scale)))
- self.mostrarTexto(_("Sobre este juego"),
- self.fuente40,
- (int(300*scale+shift_x),int(825*scale+shift_y)),
- (100,200,100))
- self.pantalla.fill((20,20,20),
- (int(610*scale+shift_x),
- int(801*scale+shift_y),
- int(590*scale),int(48*scale)))
- self.mostrarTexto(_("Salir"),
- self.fuente40,
- (int(900*scale+shift_x),int(825*scale+shift_y)),
- (100,200,100))
+ self.screen.blit(self.canvas, (0, 0))
pygame.display.flip()
- while 1:
- for event in wait_events():
- if event.type == pygame.KEYDOWN:
- if event.key == 27: # escape: volver
- self.click.play()
- self.elegir_directorio = True
- return
- elif event.type == pygame.MOUSEBUTTONDOWN:
- self.click.play()
- pos = event.pos
- if pos[1] > 275*scale + shift_y: # zona de opciones
- if pos[0] < 600*scale + shift_x: # primera columna
- if pos[1] < 275*scale + shift_y + \
- len(self.listaNiveles)*50*scale: # nivel
- self.indiceNivelActual = \
- int((pos[1]-int(275*scale+shift_y))//\
- int(50*scale))
- self.jugar = True
- return
- elif pos[1] > 800*scale + shift_y and \
- pos[1] < 850*scale + shift_y: # acerca de
- self.pantallaAcercaDe()
- else: # segunda columna
- if pos[1] > 800*scale + shift_y and \
- pos[1] < 850*scale+shift_y: # volver
- self.elegir_directorio = True
- return
- elif event.type == EVENTOREFRESCO:
- pygame.display.flip()
- def cargarImagen(self,nombre):
- """Carga una imagen y la escala de acuerdo a la resolucion"""
- global scale, xo_resolution
- if xo_resolution:
- imagen = pygame.image.load( \
- os.path.join(self.camino_imagenes,nombre))
- else:
- imagen0 = pygame.image.load( \
- os.path.join(self.camino_imagenes,nombre))
- imagen = pygame.transform.scale(imagen0,
- (int(imagen0.get_width()*scale),
- int(imagen0.get_height()*scale)))
- del imagen0
- return imagen
+ self.clock = pygame.time.Clock()
+ self.active_character = None
- def __init__(self):
- """Esta es la inicializacion del juego"""
- global scale, shift_x, shift_y, xo_resolution
- pygame.init()
- # crear pantalla
- self.anchoPantalla = gtk.gdk.screen_width()
- self.altoPantalla = gtk.gdk.screen_height()
- self.pantalla = pygame.display.set_mode((self.anchoPantalla,
- self.altoPantalla))
- if self.anchoPantalla==1200 and self.altoPantalla==900:
- xo_resolution = True
- scale = 1
- shift_x = 0
- shift_y = 0
- else:
- xo_resolution = False
- if self.anchoPantalla/1200.0<self.altoPantalla/900.0:
- scale = self.anchoPantalla/1200.0
- shift_x = 0
- shift_y = int((self.altoPantalla-scale*900)/2)
- else:
- scale = self.altoPantalla/900.0
- shift_x = int((self.anchoPantalla-scale*1200)/2)
- shift_y = 0
- # cargar imagenes generales
- self.camino_imagenes = os.path.join(CAMINORECURSOS,
- CAMINOCOMUN,
- CAMINOIMAGENES)
- self.bicho = self.cargarImagen("bicho.png")
- self.globito = self.cargarImagen("globito.png")
- self.terron = self.cargarImagen("terron.png")
- # cargar sonidos
- self.camino_sonidos = os.path.join(CAMINORECURSOS,
- CAMINOCOMUN,
- CAMINOSONIDOS)
- self.click = pygame.mixer.Sound(os.path.join(\
- self.camino_sonidos,"junggle_btn045.wav"))
- self.click.set_volume(0.2)
- # cargar fuentes
- self.fuente48 = pygame.font.Font(os.path.join(CAMINORECURSOS,\
- CAMINOCOMUN,\
- CAMINOFUENTES,\
- "AllCaps.ttf"),
- int(48*scale))
- self.fuente40 = pygame.font.Font(os.path.join(CAMINORECURSOS,\
- CAMINOCOMUN,\
- CAMINOFUENTES,\
- "Share-Regular.ttf"),
- int(34*scale))
- self.fuente32 = pygame.font.Font(None, int(32*scale))
- self.fuente24 = pygame.font.Font(None, int(24*scale))
- # cursor
- datos_cursor = (
- "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ",
- "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ",
- "XXX.........................XXXX",
- "XXX..........................XXX",
- "XXX..........................XXX",
- "XXX.........................XXXX",
- "XXX.......XXXXXXXXXXXXXXXXXXXXX ",
- "XXX........XXXXXXXXXXXXXXXXXXX ",
- "XXX.........XXX ",
- "XXX..........XXX ",
- "XXX...........XXX ",
- "XXX....X.......XXX ",
- "XXX....XX.......XXX ",
- "XXX....XXX.......XXX ",
- "XXX....XXXX.......XXX ",
- "XXX....XXXXX.......XXX ",
- "XXX....XXXXXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX ",
- "XXX....XXX XXX.......XXX",
- "XXX....XXX XXX......XXX",
- "XXX....XXX XXX.....XXX",
- "XXX....XXX XXX...XXXX",
- " XXX..XXX XXXXXXXX ",
- " XXXXXX XXXXXX ",
- " XXXX XXXX ")
- cursor = pygame.cursors.compile(datos_cursor)
- pygame.mouse.set_cursor((32,32), (1,1), *cursor)
+ # GUI functionality
+ self.gui = gui.App(theme=gui.Theme('gui.theme'))
+ guicontainer = gui.Container(align=-1, valign=-1)
+
+ credits_button = gui.Button(_('Creditos'))
+ credits_button.connect(gui.CLICK, self.show_credits_cb)
+ guicontainer.add(credits_button, scale(800)+self.layout.rect.left,
+ scale(25)+self.layout.rect.top)
+
+ self.credits_texts = []
+ self.credits_area = None
+
+ #TEXTAREA.x += self.layout.rect.left
+ #TEXTAREA.y += self.layout.rect.top
+ questionArea = gui.TextArea(value=self.text)
+ questionArea.rect = pygame.rect.Rect(TEXTAREA)
+ #questionArea.connect(gui.CHANGE, self.update_text_cb)
+ guicontainer.add(questionArea, questionArea.rect.x, questionArea.rect.y)
+ self.questionArea = questionArea
+
+ self.guicontainer = guicontainer
+
+ self.gui.init(guicontainer)
+
+ pygame.display.flip()
def cargarDirectorio(self):
"""Carga la informacion especifica de un directorio"""
self.camino_imagenes = os.path.join(CAMINORECURSOS,CAMINOLAMINA)
self.camino_datos = os.path.join(CAMINORECURSOS,CAMINOLAMINA)
- self.fondo = self.cargarImagen("mexico_estados_solo.png")
self.cargarZonas()
self.cargarNiveles()
+
def mostrarGlobito(self,lineas):
"""Muestra texto en el globito"""
- global scale, shift_x, shift_y
- self.pantalla.blit(self.globito,
- (int(XMAPAMAX*scale+shift_x),
- int(YGLOBITO*scale+shift_y)))
- yLinea = int(YGLOBITO*scale) + shift_y + \
- self.fuente32.get_height()*3
+ global shift_x, shift_y
+
+
+
+ #yLinea = TEXTAREA.y
+ self.questionArea.value = ""
+
for l in lineas:
- text = self.fuente32.render(l, 1, COLORPREGUNTAS)
- textrect = text.get_rect()
- textrect.center = (int(XCENTROPANEL*scale+shift_x),yLinea)
- self.pantalla.blit(text, textrect)
- yLinea = yLinea + self.fuente32.get_height() + int(10*scale)
+ #text = pygame.font.Font(None, scale(32)).render(l, 1, COLORPREGUNTAS)
+ #textrect = text.get_rect()
+ #textrect.x = TEXTAREA.x
+ #textrect.y = yLinea
+ self.questionArea.value = self.questionArea.value + '\n' + l.encode('latin-1')
+ #self.screen.blit(self.questionArea, textrect)
+ #yLinea = yLinea + pygame.font.Font(None, scale(32)).get_height() + scale(10)
pygame.display.flip()
def borrarGlobito(self):
""" Borra el globito, lo deja en blanco"""
- global scale, shift_x, shift_y
- self.pantalla.blit(self.globito,
- (int(XMAPAMAX*scale+shift_x),
- int(YGLOBITO*scale+shift_y)))
+ global shift_x, shift_y
+ #pos = (TEXTAREA.x, TEXTAREA.y)
+ #self.screen.blit(self.questionArea, (pos))
+ self.questionArea.value = ""
+
def correcto(self):
"""Muestra texto en el globito cuando la respuesta es correcta"""
- global scale, shift_x, shift_y
+ global shift_x, shift_y
self.correctoActual = random.randint(1,self.numeroCorrecto)-1
self.mostrarGlobito([self.listaCorrecto[self.correctoActual]])
self.esCorrecto = True
@@ -532,22 +503,63 @@ class ConozcoMx():
else:
return False
+ def credits_tab_cb(self):
+ credits = self.credits_group.value
+ self.credits_hide()
+ self.credits_show(credits)
+
+ def credits_show(self, credits_to_show):
+ self.credits_area = gui.Container(
+ width=self.background.rect.width,
+ height=self.background.rect.height)
+ self.credits_group = gui.Group()
+ self.credits_group.connect(gui.CHANGE, self.credits_tab_cb)
+ tt = gui.Table(width=self.background.rect.width,
+ height=self.background.rect.height,
+ background=(255, 255, 255))
+ tt.tr()
+ for credits in self.credits_texts:
+ b = gui.Tool(self.credits_group, gui.Label(credits[0]), credits)
+ tt.td(b)
+ tt.tr()
+ htmltext = html.HTML(credits_to_show[1], align=-1,
+ valign=-1, width=self.background.rect.width-30)
+ self.credits_box = gui.ScrollArea(htmltext,
+ width=self.background.rect.width,
+ height=self.background.rect.height-30)
+ tt.td(self.credits_box, style={'border':1}, colspan=len(self.credits_texts))
+
+ #self.lesson_plan_area = gui.ScrollArea(htmltext,
+ # self.background.rect.width, self.background.rect.height)
+ self.credits_area.add(tt, 0, 0)
+ self.guicontainer.add(self.credits_area,
+ self.background.rect.left,
+ self.background.rect.top)
+
+ def credits_hide(self):
+ # We are showing it already so hide it
+ self.guicontainer.remove(self.credits_area)
+ del self.credits_area
+ self.credits_area = None
+
+ def show_credits_cb(self):
+ if not self.credits_texts:
+ self.credits_texts = []
+ for credits in os.listdir('credits'):
+ filename = os.path.join('credits', credits, 'default.html')
+ self.credits_texts.append((credits, open(filename).read()))
+ self.credits_texts.sort()
+ if self.credits_area:
+ self.credits_hide()
+ else:
+ self.credits_show(self.credits_texts[0])
+
def jugarNivel(self):
"""Juego principal de preguntas y respuestas"""
self.nivelActual = self.listaNiveles[self.indiceNivelActual]
self.avanceNivel = 0
self.nivelActual.prepararPreguntas()
- self.pantalla.fill((100,20,20),
- (int(975*scale+shift_x),
- int(26*scale+shift_y),
- int(200*scale),
- int(48*scale)))
- self.mostrarTexto(_("Terminar"),
- self.fuente40,
- (int(1075*scale+shift_x),
- int(50*scale+shift_y)),
- (255,155,155))
- pygame.display.flip()
+
# presentar pregunta inicial
self.lineasPregunta = self.nivelActual.siguientePregunta(\
self.listaSufijos,self.listaPrefijos)
@@ -555,27 +567,29 @@ class ConozcoMx():
self.nRespuestasMal = 0
# leer eventos y ver si la respuesta es correcta
while 1:
+ #self.clock.tick(FPS)
for event in wait_events():
- if event.type == pygame.KEYDOWN:
- if event.key == 27: # escape: salir
- self.click.play()
- pygame.time.set_timer(EVENTORESPUESTA,0)
- return
+ if event.type == QUIT:
+ return
+ elif event.type == KEYDOWN and event.key == K_ESCAPE:
+ return
elif event.type == pygame.MOUSEBUTTONDOWN:
- self.click.play()
if self.avanceNivel < TOTALAVANCE:
- if event.pos[0] < XMAPAMAX*scale+shift_x: # zona mapa
+ # if the total number of correct answers hasn't been given yet
+ # check to see if the click was inside of the map / background
+ if (BACKGROUNDAREA.collidepoint(event.pos)):
self.borrarGlobito()
if self.esCorrecta(self.nivelActual,
event.pos):
self.correcto()
else:
self.mal()
- elif event.pos[0] > 975*scale+shift_x and \
- event.pos[0] < 1175*scale+shift_x and \
- event.pos[1] > 25*scale+shift_y and \
- event.pos[1] < 75*scale+shift_y: # terminar
- return
+ # if the click was not in the background, check to see
+ # if it was over a widget
+ else:
+ for widget in self.widgets:
+ if widget.is_mouse_over():
+ widget.click()
elif event.type == EVENTORESPUESTA:
pygame.time.set_timer(EVENTORESPUESTA,0)
if self.esCorrecto:
@@ -606,33 +620,103 @@ class ConozcoMx():
self.mostrarGlobito(self.lineasPregunta)
elif event.type == EVENTOREFRESCO:
pygame.display.flip()
+ self.gui.event(event)
+
+ self.bgsprites.draw(self.screen)
+ self.gui.paint(self.screen)
+ #pygame.display.flip()
+
+ def save(self, filename):
+ """Save the story.
+ """
+ f = open(filename, 'w')
+ pickle.dump(self.current_theme, f)
+ pickle.dump(self.text, f)
+ pickle.dump(len(self.stickersprites.sprites()), f)
+ for character in self.stickersprites.sprites():
+ pickle.dump(character.name, f)
+ pickle.dump(character.rect.topleft, f)
+ f.close()
def principal(self):
"""Este es el loop principal del juego"""
- global scale, shift_x, shift_y
+ global shift_x, shift_y
pygame.time.set_timer(EVENTOREFRESCO,TIEMPOREFRESCO)
while 1:
self.cargarDirectorio()
while 1:
- # pantalla inicial de juego
- self.elegir_directorio = False
- self.pantallaInicial()
- if self.elegir_directorio: # volver a seleccionar mapa
- sys.exit()
- # dibujar fondo y panel
- self.pantalla.blit(self.fondo, (shift_x, shift_y))
- self.pantalla.fill(COLORPANEL,
- (int(XMAPAMAX*scale+shift_x),shift_y,
- int(DXPANEL*scale),int(900*scale)))
- self.pantalla.blit(self.bicho,
- (int(XBICHO*scale+shift_x),
- int(YBICHO*scale+shift_y)))
- # mostrar pantalla
+ self.screen.blit(self.canvas, (0, 0))
+ self.bgsprites.draw(self.screen)
+ if hasattr(self, "gui"):
+ self.gui.paint(self.screen)
+
pygame.display.flip()
- # ir al juego
+
self.jugarNivel()
-
+
+ def update_text_cb(self):
+ self.text = self.text_input.value
+
+ def load_buttons(self):
+ """Load the character button images for the theme"""
+ buttons = []
+ for button_def in self.theme.buttons:
+ button = CharacterButton(button_def)
+ buttons.append(button)
+ return buttons
+
+ def clear(self):
+ """Remove all the characters"""
+ self.stickersprites.empty()
+ self.active_character = None
+
+ def prev_theme(self):
+ self.current_theme -= 1
+ self.current_theme %= len(self._themes)
+ self.set_theme()
+
+ def next_theme(self):
+ self.current_theme += 1
+ self.current_theme %= len(self._themes)
+ self.set_theme()
+
+ def set_theme(self, initial = False):
+ # change the theme--background map, zones,
+ # icon, and questions
+ self.theme = self._themes[self.current_theme]
+ self.icon = Icon(theme=self.theme, layout=self.layout)
+ self.background = Background(theme=self.theme, layout=self.layout)
+ self.bgsprites = pygame.sprite.OrderedUpdates(
+ [self.layout, self.background, self.icon])
+ self.widgets = [
+ Widget(THEME_LEFT, self.layout, self.prev_theme),
+ Widget(THEME_RIGHT, self.layout, self.next_theme),
+ ]
+
+ #if this is not the start-up, update the background map, icon, and
+ # questions for the new theme
+ if not initial:
+ self.camino_imagenes = os.path.join(CAMINORECURSOS,CAMINOLAMINA)
+ self.camino_datos = os.path.join(CAMINORECURSOS,CAMINOLAMINA)
+ self.cargarZonas()
+ self.cargarNiveles()
+
+ self.screen.blit(self.canvas, (0, 0))
+ self.bgsprites.draw(self.screen)
+
+ pygame.display.flip()
+
+ self.principal()
+
+
+def load_themes():
+ """Create a list of Themes from theme_defs"""
+ themes = []
+ for t in theme_list:
+ themes.append(Theme(t))
+ return themes
+
def main():
juego = ConozcoMx()
juego.principal()
diff --git a/credits/Thanks/default.html b/credits/Thanks/default.html
new file mode 100644
index 0000000..d41c915
--- /dev/null
+++ b/credits/Thanks/default.html
@@ -0,0 +1,13 @@
+<html>
+<body>
+<b><u>Conozco Mexico Credits</u></b>
+<p>Autor: Cole</p>
+
+<p>Con la ayuda y el consejo del Profesor Jose Icaza, Tec de Monterrey, Unidad Monterrey</p>
+
+<p>Mapa de Mexico, original: Alex Covarrubias (Wikimedia Commons)</p>
+<p>Tema de la interfaz grafical: Story-Builder</p>
+<p>El codigo es basado en StoryBuilder y Conozco / Conozco-Numeros</p>
+
+</body>
+</html>
diff --git a/gui.theme/Vera.ttf b/gui.theme/Vera.ttf
new file mode 100644
index 0000000..58cd6b5
--- /dev/null
+++ b/gui.theme/Vera.ttf
Binary files differ
diff --git a/gui.theme/box.down.png b/gui.theme/box.down.png
new file mode 100644
index 0000000..b9e965d
--- /dev/null
+++ b/gui.theme/box.down.png
Binary files differ
diff --git a/gui.theme/box.hover.png b/gui.theme/box.hover.png
new file mode 100644
index 0000000..ef3c225
--- /dev/null
+++ b/gui.theme/box.hover.png
Binary files differ
diff --git a/gui.theme/box.normal.png b/gui.theme/box.normal.png
new file mode 100644
index 0000000..90f8d40
--- /dev/null
+++ b/gui.theme/box.normal.png
Binary files differ
diff --git a/gui.theme/box.xcf b/gui.theme/box.xcf
new file mode 100644
index 0000000..960ca8d
--- /dev/null
+++ b/gui.theme/box.xcf
Binary files differ
diff --git a/gui.theme/button.down.png b/gui.theme/button.down.png
new file mode 100644
index 0000000..2ae436d
--- /dev/null
+++ b/gui.theme/button.down.png
Binary files differ
diff --git a/gui.theme/button.hover.png b/gui.theme/button.hover.png
new file mode 100644
index 0000000..21d01fd
--- /dev/null
+++ b/gui.theme/button.hover.png
Binary files differ
diff --git a/gui.theme/button.normal.png b/gui.theme/button.normal.png
new file mode 100644
index 0000000..21d01fd
--- /dev/null
+++ b/gui.theme/button.normal.png
Binary files differ
diff --git a/gui.theme/check.png b/gui.theme/check.png
new file mode 100644
index 0000000..4ef58a3
--- /dev/null
+++ b/gui.theme/check.png
Binary files differ
diff --git a/gui.theme/checkbox.off.hover.tga b/gui.theme/checkbox.off.hover.tga
new file mode 100644
index 0000000..9a4d8a8
--- /dev/null
+++ b/gui.theme/checkbox.off.hover.tga
Binary files differ
diff --git a/gui.theme/checkbox.off.normal.tga b/gui.theme/checkbox.off.normal.tga
new file mode 100644
index 0000000..de59f19
--- /dev/null
+++ b/gui.theme/checkbox.off.normal.tga
Binary files differ
diff --git a/gui.theme/checkbox.on.hover.tga b/gui.theme/checkbox.on.hover.tga
new file mode 100644
index 0000000..4940c26
--- /dev/null
+++ b/gui.theme/checkbox.on.hover.tga
Binary files differ
diff --git a/gui.theme/checkbox.on.normal.tga b/gui.theme/checkbox.on.normal.tga
new file mode 100644
index 0000000..9cf658e
--- /dev/null
+++ b/gui.theme/checkbox.on.normal.tga
Binary files differ
diff --git a/gui.theme/config.txt b/gui.theme/config.txt
new file mode 100644
index 0000000..13f49bc
--- /dev/null
+++ b/gui.theme/config.txt
@@ -0,0 +1,291 @@
+desktop background desktop.png
+
+input font Vera.ttf 16
+input background input.normal.png
+input color #000000
+input:focus background input.focus.png
+input padding_left 6
+input padding_right 6
+input padding_top 3
+input padding_bottom 3
+
+link font Vera.ttf 24
+link color #0000FF
+link:hover color #FF0000
+link:down color #00FF00
+
+label font Vera.ttf 16
+label color #000000
+
+document font Vera.ttf 16
+document color #000000
+div font Vera.ttf 16
+div color #000000
+
+td font Vera.ttf 16
+td color #000000
+th font Vera.ttf 16
+th color #000000
+
+h1 font Vera.ttf 24
+h1 color #000000
+h2 font Vera.ttf 20
+h2 color #000000
+h3 font Vera.ttf 16
+h3 color #000000
+h4 font Vera.ttf 14
+h4 color #000000
+h5 font Vera.ttf 12
+h5 color #000000
+h6 font Vera.ttf 10
+h6 color #000000
+
+ul font Vera.ttf 16
+ul color #000000
+ol font Vera.ttf 16
+ol color #000000
+li font Vera.ttf 16
+li color #000000
+li padding_left 32
+
+pre font mono 16
+pre color #000000
+code font mono 16
+code color #000000
+
+checkbox off checkbox.off.normal.tga
+checkbox on checkbox.on.normal.tga
+checkbox:hover off checkbox.off.hover.tga
+checkbox:hover on checkbox.on.hover.tga
+checkbox:down off checkbox.off.hover.tga
+checkbox:down on checkbox.on.hover.tga
+
+switch off checkbox.off.normal.tga
+switch on checkbox.on.normal.tga
+switch:hover off checkbox.off.hover.tga
+switch:hover on checkbox.on.hover.tga
+switch:down off checkbox.off.hover.tga
+switch:down on checkbox.on.hover.tga
+
+radio off radio.off.normal.tga
+radio on radio.on.normal.tga
+radio:hover off radio.off.hover.tga
+radio:hover on radio.on.hover.tga
+radio:down off radio.off.hover.tga
+radio:down on radio.on.hover.tga
+
+button background button.normal.png
+button:hover background button.hover.png
+button:down background button.down.png
+button padding_left 50
+button padding_right 50
+button padding_top 4
+button padding_bottom 4
+button.label font Vera.ttf 16
+button.label color #FFFFFF
+
+slider background slider.tga
+slider bar slider.bar.normal.tga
+slider:hover bar slider.bar.hover.tga
+slider width 16
+slider height 16
+
+hslider background hslider.tga
+hslider bar hslider.bar.normal.tga
+hslider:hover bar hslider.bar.hover.tga
+hslider:down bar hslider.bar.hover.tga
+hslider width 16
+hslider height 16
+
+vslider background vslider.tga
+vslider bar vslider.bar.normal.tga
+vslider:hover bar vslider.bar.hover.tga
+vslider:down bar vslider.bar.hover.tga
+vslider width 16
+vslider height 16
+
+xhscrollbar height 16
+xhscrollbar background scroller.slide.h.tga
+xhscrollbar bar scroller.slide.bar.normal.tga
+xhscrollbar:hover bar scroller.slide.bar.hover.tga
+
+xvscrollbar width 16
+xvscrollbar background scroller.slide.v.tga
+xvscrollbar bar scroller.slide.bar.normal.tga
+xvscrollbar:hover bar scroller.slide.bar.hover.tga
+
+hscrollbar.slider background hslider.tga
+hscrollbar.slider bar hslider.bar.normal.tga
+hscrollbar.slider:hover bar hslider.bar.hover.tga
+hscrollbar.slider:down bar hslider.bar.hover.tga
+hscrollbar.slider width 16
+hscrollbar.slider height 16
+hscrollbar minus hslider.left.tga
+hscrollbar plus hslider.right.tga
+
+vscrollbar.slider background vslider.tga
+vscrollbar.slider bar vslider.bar.normal.tga
+vscrollbar.slider:hover bar vslider.bar.hover.tga
+vscrollbar.slider:down bar vslider.bar.hover.tga
+vscrollbar.slider width 16
+vscrollbar.slider height 16
+vscrollbar minus vslider.up.tga
+vscrollbar plus vslider.down.tga
+
+
+select.selected background select.selected.normal.tga
+select.selected:hover background select.selected.hover.tga
+select.selected:down background select.selected.down.tga
+select.selected padding_left 4
+select.selected padding_right 4
+select.selected padding_top 1
+select.selected padding_bottom 1
+select.arrow background select.arrow.normal.tga
+select.arrow:hover background select.arrow.hover.tga
+select.arrow:down background select.arrow.down.tga
+select.arrow padding_left 1
+select.arrow padding_right 1
+
+select.options background select.options.png
+select.option background select.option.normal.png
+select.option:hover background select.option.hover.png
+select.option:down background select.option.hover.png
+select.option padding_left 4
+select.option padding_right 4
+select.option padding_top 1
+select.option padding_bottom 1
+#select.option border_top 1
+#select.option border_right 1
+#select.option border_bottom 1
+#select.option border_left 1
+
+select.option.label font Vera.ttf 16
+select.option.label color #000000
+select.options padding_left 1
+select.options padding_right 1
+select.options padding_top 1
+select.options padding_bottom 1
+select arrow select.arrow.png
+
+
+dialog background dialog.bar.png
+xdialog.bar background dialog.bar.png
+dialog.bar padding_left 8
+dialog.bar padding_right 8
+dialog.bar padding_top 2
+dialog.bar padding_bottom 1
+dialog.bar.close image dialog.close.normal.tga
+dialog.bar.close:hover image dialog.close.hover.tga
+dialog.bar.close:down image dialog.close.down.tga
+dialog.main background dialog.png
+dialog.main padding_left 8
+dialog.main padding_right 8
+dialog.main padding_top 4
+dialog.main padding_bottom 4
+
+keysym font Vera.ttf 16
+keysym background input.normal.png
+keysym color #000000
+keysym:focus background input.focus.png
+keysym padding_left 6
+keysym padding_right 6
+keysym padding_top 3
+keysym padding_bottom 3
+
+tool background tool.normal.tga
+tool:hover background tool.hover.tga
+tool:down background tool.down.tga
+tool padding_left 4
+tool padding_right 4
+tool padding_top 1
+tool padding_bottom 1
+tool.label font Vera.ttf 16
+tool.label color #000000
+
+menu background menu.normal.tga
+menu:hover background menu.hover.tga
+menu:down background menu.down.tga
+menu padding_left 6
+menu padding_right 6
+menu padding_top 3
+menu padding_bottom 3
+menu.label font Vera.ttf 16
+menu.label color #000000
+
+menu-open background menu.down.tga
+menu-open:hover background menu.down.tga
+menu-open:down background menu.down.tga
+menu-open padding_left 6
+menu-open padding_right 6
+menu-open padding_top 3
+menu-open padding_bottom 3
+
+menu.options background select.options.png
+menu.option background select.option.normal.png
+menu.option:hover background select.option.hover.png
+menu.option:down background select.option.hover.png
+menu.option padding_left 6
+menu.option padding_right 6
+menu.option padding_top 1
+menu.option padding_bottom 1
+menu.option.label font Vera.ttf 16
+menu.option.label color #000000
+menu.options padding_left 1
+menu.options padding_right 1
+menu.options padding_top 1
+menu.options padding_bottom 1
+menu arrow select.arrow.tga
+
+
+scrollarea.content background #ffffff
+scrollarea.content padding_left 1
+scrollarea.content padding_right 1
+scrollarea.content padding_top 1
+scrollarea.content padding_bottom 1
+
+
+list.item background list.item.normal.png
+list.item:hover background list.item.down.png
+list.item:down background list.item.down.png
+list.item padding_left 4
+list.item padding_right 4
+list.item padding_top 2
+list.item padding_bottom 2
+list.item margin_bottom 1
+list.item align -1
+list.item.label font Vera.ttf 14
+list.item.label color #000000
+
+list background list.png
+list padding_left 1
+list padding_right 1
+list padding_top 1
+list padding_bottom 1
+list.content background #eeeeee
+list.content padding_left 1
+list.content padding_right 1
+list.content padding_top 1
+list.content padding_bottom 1
+
+filedialog.folder image filebrowser.folder.png
+filedialog.label font Vera.ttf 14
+filedialog.label color #000000
+filedialog.title.label font Vera.ttf 16
+filedialog.title.label color #000000
+filedialog.input font Vera.ttf 14
+filedialog.input background input.normal.png
+filedialog.input color #000000
+filedialog.input:focus background input.focus.png
+filedialog.input padding_left 6
+filedialog.input padding_right 6
+filedialog.input padding_top 3
+filedialog.input padding_bottom 3
+
+dialog.title.label font Vera.ttf 16
+dialog.title.label color #000000
+
+
+progressbar background progressbar.tga
+progressbar bar progressbar.bar.tga
+progressbar width 16
+progressbar height 16
diff --git a/gui.theme/config.txt~ b/gui.theme/config.txt~
new file mode 100644
index 0000000..b5db8c7
--- /dev/null
+++ b/gui.theme/config.txt~
@@ -0,0 +1,291 @@
+desktop background desktop.png
+
+input font Vera.ttf 16
+input background input.normal.png
+input color #000000
+input:focus background input.focus.png
+input padding_left 6
+input padding_right 6
+input padding_top 3
+input padding_bottom 3
+
+link font Vera.ttf 24
+link color #0000FF
+link:hover color #FF0000
+link:down color #00FF00
+
+label font Vera.ttf 16
+label color #000000
+
+document font Vera.ttf 16
+document color #000000
+div font Vera.ttf 16
+div color #000000
+
+td font Vera.ttf 16
+td color #000000
+th font Vera.ttf 16
+th color #000000
+
+h1 font Vera.ttf 24
+h1 color #000000
+h2 font Vera.ttf 20
+h2 color #000000
+h3 font Vera.ttf 16
+h3 color #000000
+h4 font Vera.ttf 14
+h4 color #000000
+h5 font Vera.ttf 12
+h5 color #000000
+h6 font Vera.ttf 10
+h6 color #000000
+
+ul font Vera.ttf 16
+ul color #000000
+ol font Vera.ttf 16
+ol color #000000
+li font Vera.ttf 16
+li color #000000
+li padding_left 32
+
+pre font mono 16
+pre color #000000
+code font mono 16
+code color #000000
+
+checkbox off checkbox.off.normal.tga
+checkbox on checkbox.on.normal.tga
+checkbox:hover off checkbox.off.hover.tga
+checkbox:hover on checkbox.on.hover.tga
+checkbox:down off checkbox.off.hover.tga
+checkbox:down on checkbox.on.hover.tga
+
+switch off checkbox.off.normal.tga
+switch on checkbox.on.normal.tga
+switch:hover off checkbox.off.hover.tga
+switch:hover on checkbox.on.hover.tga
+switch:down off checkbox.off.hover.tga
+switch:down on checkbox.on.hover.tga
+
+radio off radio.off.normal.tga
+radio on radio.on.normal.tga
+radio:hover off radio.off.hover.tga
+radio:hover on radio.on.hover.tga
+radio:down off radio.off.hover.tga
+radio:down on radio.on.hover.tga
+
+button background button.normal.png
+button:hover background button.hover.png
+button:down background button.down.png
+button padding_left 50
+button padding_right 50
+button padding_top 8
+button padding_bottom 8
+button.label font Vera.ttf 16
+button.label color #FFFFFF
+
+slider background slider.tga
+slider bar slider.bar.normal.tga
+slider:hover bar slider.bar.hover.tga
+slider width 16
+slider height 16
+
+hslider background hslider.tga
+hslider bar hslider.bar.normal.tga
+hslider:hover bar hslider.bar.hover.tga
+hslider:down bar hslider.bar.hover.tga
+hslider width 16
+hslider height 16
+
+vslider background vslider.tga
+vslider bar vslider.bar.normal.tga
+vslider:hover bar vslider.bar.hover.tga
+vslider:down bar vslider.bar.hover.tga
+vslider width 16
+vslider height 16
+
+xhscrollbar height 16
+xhscrollbar background scroller.slide.h.tga
+xhscrollbar bar scroller.slide.bar.normal.tga
+xhscrollbar:hover bar scroller.slide.bar.hover.tga
+
+xvscrollbar width 16
+xvscrollbar background scroller.slide.v.tga
+xvscrollbar bar scroller.slide.bar.normal.tga
+xvscrollbar:hover bar scroller.slide.bar.hover.tga
+
+hscrollbar.slider background hslider.tga
+hscrollbar.slider bar hslider.bar.normal.tga
+hscrollbar.slider:hover bar hslider.bar.hover.tga
+hscrollbar.slider:down bar hslider.bar.hover.tga
+hscrollbar.slider width 16
+hscrollbar.slider height 16
+hscrollbar minus hslider.left.tga
+hscrollbar plus hslider.right.tga
+
+vscrollbar.slider background vslider.tga
+vscrollbar.slider bar vslider.bar.normal.tga
+vscrollbar.slider:hover bar vslider.bar.hover.tga
+vscrollbar.slider:down bar vslider.bar.hover.tga
+vscrollbar.slider width 16
+vscrollbar.slider height 16
+vscrollbar minus vslider.up.tga
+vscrollbar plus vslider.down.tga
+
+
+select.selected background select.selected.normal.tga
+select.selected:hover background select.selected.hover.tga
+select.selected:down background select.selected.down.tga
+select.selected padding_left 4
+select.selected padding_right 4
+select.selected padding_top 1
+select.selected padding_bottom 1
+select.arrow background select.arrow.normal.tga
+select.arrow:hover background select.arrow.hover.tga
+select.arrow:down background select.arrow.down.tga
+select.arrow padding_left 1
+select.arrow padding_right 1
+
+select.options background select.options.png
+select.option background select.option.normal.png
+select.option:hover background select.option.hover.png
+select.option:down background select.option.hover.png
+select.option padding_left 4
+select.option padding_right 4
+select.option padding_top 1
+select.option padding_bottom 1
+#select.option border_top 1
+#select.option border_right 1
+#select.option border_bottom 1
+#select.option border_left 1
+
+select.option.label font Vera.ttf 16
+select.option.label color #000000
+select.options padding_left 1
+select.options padding_right 1
+select.options padding_top 1
+select.options padding_bottom 1
+select arrow select.arrow.png
+
+
+dialog background dialog.bar.png
+xdialog.bar background dialog.bar.png
+dialog.bar padding_left 8
+dialog.bar padding_right 8
+dialog.bar padding_top 2
+dialog.bar padding_bottom 1
+dialog.bar.close image dialog.close.normal.tga
+dialog.bar.close:hover image dialog.close.hover.tga
+dialog.bar.close:down image dialog.close.down.tga
+dialog.main background dialog.png
+dialog.main padding_left 8
+dialog.main padding_right 8
+dialog.main padding_top 4
+dialog.main padding_bottom 4
+
+keysym font Vera.ttf 16
+keysym background input.normal.png
+keysym color #000000
+keysym:focus background input.focus.png
+keysym padding_left 6
+keysym padding_right 6
+keysym padding_top 3
+keysym padding_bottom 3
+
+tool background tool.normal.tga
+tool:hover background tool.hover.tga
+tool:down background tool.down.tga
+tool padding_left 4
+tool padding_right 4
+tool padding_top 1
+tool padding_bottom 1
+tool.label font Vera.ttf 16
+tool.label color #000000
+
+menu background menu.normal.tga
+menu:hover background menu.hover.tga
+menu:down background menu.down.tga
+menu padding_left 6
+menu padding_right 6
+menu padding_top 3
+menu padding_bottom 3
+menu.label font Vera.ttf 16
+menu.label color #000000
+
+menu-open background menu.down.tga
+menu-open:hover background menu.down.tga
+menu-open:down background menu.down.tga
+menu-open padding_left 6
+menu-open padding_right 6
+menu-open padding_top 3
+menu-open padding_bottom 3
+
+menu.options background select.options.png
+menu.option background select.option.normal.png
+menu.option:hover background select.option.hover.png
+menu.option:down background select.option.hover.png
+menu.option padding_left 6
+menu.option padding_right 6
+menu.option padding_top 1
+menu.option padding_bottom 1
+menu.option.label font Vera.ttf 16
+menu.option.label color #000000
+menu.options padding_left 1
+menu.options padding_right 1
+menu.options padding_top 1
+menu.options padding_bottom 1
+menu arrow select.arrow.tga
+
+
+scrollarea.content background #ffffff
+scrollarea.content padding_left 1
+scrollarea.content padding_right 1
+scrollarea.content padding_top 1
+scrollarea.content padding_bottom 1
+
+
+list.item background list.item.normal.png
+list.item:hover background list.item.down.png
+list.item:down background list.item.down.png
+list.item padding_left 4
+list.item padding_right 4
+list.item padding_top 2
+list.item padding_bottom 2
+list.item margin_bottom 1
+list.item align -1
+list.item.label font Vera.ttf 14
+list.item.label color #000000
+
+list background list.png
+list padding_left 1
+list padding_right 1
+list padding_top 1
+list padding_bottom 1
+list.content background #eeeeee
+list.content padding_left 1
+list.content padding_right 1
+list.content padding_top 1
+list.content padding_bottom 1
+
+filedialog.folder image filebrowser.folder.png
+filedialog.label font Vera.ttf 14
+filedialog.label color #000000
+filedialog.title.label font Vera.ttf 16
+filedialog.title.label color #000000
+filedialog.input font Vera.ttf 14
+filedialog.input background input.normal.png
+filedialog.input color #000000
+filedialog.input:focus background input.focus.png
+filedialog.input padding_left 6
+filedialog.input padding_right 6
+filedialog.input padding_top 3
+filedialog.input padding_bottom 3
+
+dialog.title.label font Vera.ttf 16
+dialog.title.label color #000000
+
+
+progressbar background progressbar.tga
+progressbar bar progressbar.bar.tga
+progressbar width 16
+progressbar height 16
diff --git a/gui.theme/console.input.focus.png b/gui.theme/console.input.focus.png
new file mode 100644
index 0000000..819d835
--- /dev/null
+++ b/gui.theme/console.input.focus.png
Binary files differ
diff --git a/gui.theme/console.input.normal.png b/gui.theme/console.input.normal.png
new file mode 100644
index 0000000..a14e329
--- /dev/null
+++ b/gui.theme/console.input.normal.png
Binary files differ
diff --git a/gui.theme/console.png b/gui.theme/console.png
new file mode 100644
index 0000000..a14e329
--- /dev/null
+++ b/gui.theme/console.png
Binary files differ
diff --git a/gui.theme/desktop.png b/gui.theme/desktop.png
new file mode 100644
index 0000000..c83f5cd
--- /dev/null
+++ b/gui.theme/desktop.png
Binary files differ
diff --git a/gui.theme/desktop.xcf b/gui.theme/desktop.xcf
new file mode 100644
index 0000000..2c504ab
--- /dev/null
+++ b/gui.theme/desktop.xcf
Binary files differ
diff --git a/gui.theme/dialog.bar.png b/gui.theme/dialog.bar.png
new file mode 100644
index 0000000..86cfddb
--- /dev/null
+++ b/gui.theme/dialog.bar.png
Binary files differ
diff --git a/gui.theme/dialog.close.down.tga b/gui.theme/dialog.close.down.tga
new file mode 100644
index 0000000..ade4813
--- /dev/null
+++ b/gui.theme/dialog.close.down.tga
Binary files differ
diff --git a/gui.theme/dialog.close.hover.tga b/gui.theme/dialog.close.hover.tga
new file mode 100644
index 0000000..9f36bb7
--- /dev/null
+++ b/gui.theme/dialog.close.hover.tga
Binary files differ
diff --git a/gui.theme/dialog.close.normal.tga b/gui.theme/dialog.close.normal.tga
new file mode 100644
index 0000000..ee3a5d4
--- /dev/null
+++ b/gui.theme/dialog.close.normal.tga
Binary files differ
diff --git a/gui.theme/dialog.png b/gui.theme/dialog.png
new file mode 100644
index 0000000..26ae2a6
--- /dev/null
+++ b/gui.theme/dialog.png
Binary files differ
diff --git a/gui.theme/dot.down.png b/gui.theme/dot.down.png
new file mode 100644
index 0000000..ab117a7
--- /dev/null
+++ b/gui.theme/dot.down.png
Binary files differ
diff --git a/gui.theme/dot.hover.png b/gui.theme/dot.hover.png
new file mode 100644
index 0000000..090f07d
--- /dev/null
+++ b/gui.theme/dot.hover.png
Binary files differ
diff --git a/gui.theme/dot.normal.png b/gui.theme/dot.normal.png
new file mode 100644
index 0000000..55bd736
--- /dev/null
+++ b/gui.theme/dot.normal.png
Binary files differ
diff --git a/gui.theme/dot.xcf b/gui.theme/dot.xcf
new file mode 100644
index 0000000..3100750
--- /dev/null
+++ b/gui.theme/dot.xcf
Binary files differ
diff --git a/gui.theme/down.png b/gui.theme/down.png
new file mode 100644
index 0000000..7532249
--- /dev/null
+++ b/gui.theme/down.png
Binary files differ
diff --git a/gui.theme/filebrowser.folder.png b/gui.theme/filebrowser.folder.png
new file mode 100644
index 0000000..4a3bd2c
--- /dev/null
+++ b/gui.theme/filebrowser.folder.png
Binary files differ
diff --git a/gui.theme/generate.py b/gui.theme/generate.py
new file mode 100644
index 0000000..a161556
--- /dev/null
+++ b/gui.theme/generate.py
@@ -0,0 +1,98 @@
+import pygame
+from pygame.locals import *
+pygame.display.init()
+pygame.display.set_mode((80,80),32)
+
+def prep(name):
+ fname = name+".png"
+ img = pygame.image.load(fname)
+ w,h = img.get_width()/2,img.get_height()/2
+
+ out = pygame.Surface((w*3,h*3),SWSURFACE|SRCALPHA,32)
+ out.fill((0,0,0,0))
+ out.blit(img.subsurface(0,0,w,h),(0,0))
+ out.blit(img.subsurface(w,0,w,h),(w*2,0))
+ out.blit(img.subsurface(0,h,w,h),(0,h*2))
+ out.blit(img.subsurface(w,h,w,h),(w*2,h*2))
+ for i in range(0,w):
+ img = out.subsurface((w-1,0,1,h*3)).convert_alpha()
+ out.blit(img,(w+i,0))
+ for i in range(0,h):
+ img = out.subsurface((0,h-1,w*3,1)).convert_alpha()
+ out.blit(img,(0,h+i))
+
+ return out,w,h
+
+todo = [
+ ('button.normal','dot.normal',None,3,3,'789456123'),
+ ('button.hover','dot.hover',None,3,3,'789456123'),
+ ('button.down','dot.down',None,3,3,'789456123'),
+
+ ('checkbox.off.normal','box.normal',None,2,2,'7913'),
+ ('checkbox.on.normal','box.down','check',2,2,'7913'),
+ ('checkbox.off.hover','box.hover',None,2,2,'7913'),
+ ('checkbox.on.hover','box.hover','check',2,2,'7913'),
+
+ ('radio.off.normal','dot.normal',None,2,2,'7913'),
+ ('radio.on.normal','dot.down','radio',2,2,'7913'),
+ ('radio.off.hover','dot.hover',None,2,2,'7913'),
+ ('radio.on.hover','dot.hover','radio',2,2,'7913'),
+
+ ('tool.normal','box.normal',None,3,3,'789456123'),
+ ('tool.hover','box.hover',None,3,3,'789456123'),
+ ('tool.down','box.down',None,3,3,'789456123'),
+
+ ('hslider','idot.normal',None,3,3,'789456123'),
+ ('hslider.bar.normal','dot.normal',None,3,3,'789456123'),
+ ('hslider.bar.hover','dot.hover',None,3,3,'789456123'),
+ ('hslider.left','sbox.normal','left',2,2,'7913'),
+ ('hslider.right','sbox.normal','right',2,2,'7913'),
+
+
+ ('vslider','idot.normal',None,3,3,'789456123'),
+ ('vslider.bar.normal','vdot.normal',None,3,3,'789456123'),
+ ('vslider.bar.hover','vdot.hover',None,3,3,'789456123'),
+ ('vslider.up','vsbox.normal','up',2,2,'7913'),
+ ('vslider.down','vsbox.normal','down',2,2,'7913'),
+
+ ('dialog.close.normal','rdot.hover',None,2,2,'7913'),
+ ('dialog.close.hover','rdot.hover','x',2,2,'7913'),
+ ('dialog.close.down','rdot.down','x',2,2,'7913'),
+
+ ('menu.normal','desktop',None,1,1,'7'),
+ ('menu.hover','box.normal',None,3,3,'789456123'),
+ ('menu.down','box.down',None,3,3,'789456123'),
+
+ ('select.selected.normal','box.normal',None,3,3,'788455122'),
+ ('select.selected.hover','box.hover',None,3,3,'788455122'),
+ ('select.selected.down','box.down',None,3,3,'788455122'),
+
+ ('select.arrow.normal','box.hover',None,3,3,'889556223'),
+ ('select.arrow.hover','box.hover',None,3,3,'889556223'),
+ ('select.arrow.down','box.down',None,3,3,'889556223'),
+
+ ('progressbar','sbox.normal',None,3,3,'789456123'),
+ ('progressbar.bar','box.hover',None,3,3,'789456123'),
+ ]
+
+for fname,img,over,ww,hh,s in todo:
+ print fname
+ img,w,h = prep(img)
+ out = pygame.Surface((ww*w,hh*h),SWSURFACE|SRCALPHA,32)
+ out.fill((0,0,0,0))
+ n = 0
+ for y in range(0,hh):
+ for x in range(0,ww):
+ c = int(s[n])
+ xx,yy = (c-1)%3,2-(c-1)/3
+ out.blit(img.subsurface((xx*w,yy*h,w,h)),(x*w,y*h))
+ n += 1
+ if over != None:
+ over = pygame.image.load(over+".png")
+ out.blit(over,(0,0))
+ pygame.image.save(out,fname+".tga")
+
+
+
+
+
diff --git a/gui.theme/hslider.bar.hover.tga b/gui.theme/hslider.bar.hover.tga
new file mode 100644
index 0000000..5e5c53a
--- /dev/null
+++ b/gui.theme/hslider.bar.hover.tga
Binary files differ
diff --git a/gui.theme/hslider.bar.normal.tga b/gui.theme/hslider.bar.normal.tga
new file mode 100644
index 0000000..e9371c7
--- /dev/null
+++ b/gui.theme/hslider.bar.normal.tga
Binary files differ
diff --git a/gui.theme/hslider.left.tga b/gui.theme/hslider.left.tga
new file mode 100644
index 0000000..2fe406c
--- /dev/null
+++ b/gui.theme/hslider.left.tga
Binary files differ
diff --git a/gui.theme/hslider.right.tga b/gui.theme/hslider.right.tga
new file mode 100644
index 0000000..86a9ca5
--- /dev/null
+++ b/gui.theme/hslider.right.tga
Binary files differ
diff --git a/gui.theme/hslider.tga b/gui.theme/hslider.tga
new file mode 100644
index 0000000..ff3b4b2
--- /dev/null
+++ b/gui.theme/hslider.tga
Binary files differ
diff --git a/gui.theme/idot.normal.png b/gui.theme/idot.normal.png
new file mode 100644
index 0000000..4e22195
--- /dev/null
+++ b/gui.theme/idot.normal.png
Binary files differ
diff --git a/gui.theme/input.focus.png b/gui.theme/input.focus.png
new file mode 100644
index 0000000..477a826
--- /dev/null
+++ b/gui.theme/input.focus.png
Binary files differ
diff --git a/gui.theme/input.normal.png b/gui.theme/input.normal.png
new file mode 100644
index 0000000..7269969
--- /dev/null
+++ b/gui.theme/input.normal.png
Binary files differ
diff --git a/gui.theme/left.png b/gui.theme/left.png
new file mode 100644
index 0000000..b965666
--- /dev/null
+++ b/gui.theme/left.png
Binary files differ
diff --git a/gui.theme/list.item.down.png b/gui.theme/list.item.down.png
new file mode 100644
index 0000000..d6c062a
--- /dev/null
+++ b/gui.theme/list.item.down.png
Binary files differ
diff --git a/gui.theme/list.item.hover.png b/gui.theme/list.item.hover.png
new file mode 100644
index 0000000..627790d
--- /dev/null
+++ b/gui.theme/list.item.hover.png
Binary files differ
diff --git a/gui.theme/list.item.normal.png b/gui.theme/list.item.normal.png
new file mode 100644
index 0000000..627790d
--- /dev/null
+++ b/gui.theme/list.item.normal.png
Binary files differ
diff --git a/gui.theme/list.png b/gui.theme/list.png
new file mode 100644
index 0000000..99ad5bc
--- /dev/null
+++ b/gui.theme/list.png
Binary files differ
diff --git a/gui.theme/listitem.down.tga b/gui.theme/listitem.down.tga
new file mode 100644
index 0000000..13e2e57
--- /dev/null
+++ b/gui.theme/listitem.down.tga
Binary files differ
diff --git a/gui.theme/listitem.hover.tga b/gui.theme/listitem.hover.tga
new file mode 100644
index 0000000..8bdf60a
--- /dev/null
+++ b/gui.theme/listitem.hover.tga
Binary files differ
diff --git a/gui.theme/listitem.normal.tga b/gui.theme/listitem.normal.tga
new file mode 100644
index 0000000..a2994aa
--- /dev/null
+++ b/gui.theme/listitem.normal.tga
Binary files differ
diff --git a/gui.theme/menu.down.tga b/gui.theme/menu.down.tga
new file mode 100644
index 0000000..f89d4b4
--- /dev/null
+++ b/gui.theme/menu.down.tga
Binary files differ
diff --git a/gui.theme/menu.hover.tga b/gui.theme/menu.hover.tga
new file mode 100644
index 0000000..b304b87
--- /dev/null
+++ b/gui.theme/menu.hover.tga
Binary files differ
diff --git a/gui.theme/menu.normal.tga b/gui.theme/menu.normal.tga
new file mode 100644
index 0000000..d3eb2d0
--- /dev/null
+++ b/gui.theme/menu.normal.tga
Binary files differ
diff --git a/gui.theme/notes.txt b/gui.theme/notes.txt
new file mode 100644
index 0000000..f6541e4
--- /dev/null
+++ b/gui.theme/notes.txt
@@ -0,0 +1,8 @@
+dot and box.xcf:
+
+color -170
+
+.down
+.hover +64 brightness
+.normal, grayscale +127 brightness, +48 contrast
+
diff --git a/gui.theme/out.tga b/gui.theme/out.tga
new file mode 100644
index 0000000..7ed46cc
--- /dev/null
+++ b/gui.theme/out.tga
Binary files differ
diff --git a/gui.theme/progressbar.bar.tga b/gui.theme/progressbar.bar.tga
new file mode 100644
index 0000000..184ae9c
--- /dev/null
+++ b/gui.theme/progressbar.bar.tga
Binary files differ
diff --git a/gui.theme/progressbar.tga b/gui.theme/progressbar.tga
new file mode 100644
index 0000000..d459763
--- /dev/null
+++ b/gui.theme/progressbar.tga
Binary files differ
diff --git a/gui.theme/radio.off.hover.tga b/gui.theme/radio.off.hover.tga
new file mode 100644
index 0000000..6b0f737
--- /dev/null
+++ b/gui.theme/radio.off.hover.tga
Binary files differ
diff --git a/gui.theme/radio.off.normal.tga b/gui.theme/radio.off.normal.tga
new file mode 100644
index 0000000..3da51d8
--- /dev/null
+++ b/gui.theme/radio.off.normal.tga
Binary files differ
diff --git a/gui.theme/radio.on.hover.tga b/gui.theme/radio.on.hover.tga
new file mode 100644
index 0000000..d26764b
--- /dev/null
+++ b/gui.theme/radio.on.hover.tga
Binary files differ
diff --git a/gui.theme/radio.on.normal.tga b/gui.theme/radio.on.normal.tga
new file mode 100644
index 0000000..42515fe
--- /dev/null
+++ b/gui.theme/radio.on.normal.tga
Binary files differ
diff --git a/gui.theme/radio.png b/gui.theme/radio.png
new file mode 100644
index 0000000..7596f48
--- /dev/null
+++ b/gui.theme/radio.png
Binary files differ
diff --git a/gui.theme/rdot.down.png b/gui.theme/rdot.down.png
new file mode 100644
index 0000000..35cd4fe
--- /dev/null
+++ b/gui.theme/rdot.down.png
Binary files differ
diff --git a/gui.theme/rdot.hover.png b/gui.theme/rdot.hover.png
new file mode 100644
index 0000000..5cd77a2
--- /dev/null
+++ b/gui.theme/rdot.hover.png
Binary files differ
diff --git a/gui.theme/rdot.normal.png b/gui.theme/rdot.normal.png
new file mode 100644
index 0000000..636a207
--- /dev/null
+++ b/gui.theme/rdot.normal.png
Binary files differ
diff --git a/gui.theme/right.png b/gui.theme/right.png
new file mode 100644
index 0000000..613779e
--- /dev/null
+++ b/gui.theme/right.png
Binary files differ
diff --git a/gui.theme/sbox.normal.png b/gui.theme/sbox.normal.png
new file mode 100644
index 0000000..00be882
--- /dev/null
+++ b/gui.theme/sbox.normal.png
Binary files differ
diff --git a/gui.theme/scroller.slide.bar.hover.tga b/gui.theme/scroller.slide.bar.hover.tga
new file mode 100644
index 0000000..d0b85a9
--- /dev/null
+++ b/gui.theme/scroller.slide.bar.hover.tga
Binary files differ
diff --git a/gui.theme/scroller.slide.bar.normal.tga b/gui.theme/scroller.slide.bar.normal.tga
new file mode 100644
index 0000000..84ff6bb
--- /dev/null
+++ b/gui.theme/scroller.slide.bar.normal.tga
Binary files differ
diff --git a/gui.theme/scroller.slide.h.tga b/gui.theme/scroller.slide.h.tga
new file mode 100644
index 0000000..0281567
--- /dev/null
+++ b/gui.theme/scroller.slide.h.tga
Binary files differ
diff --git a/gui.theme/scroller.slide.v.tga b/gui.theme/scroller.slide.v.tga
new file mode 100644
index 0000000..cbaa875
--- /dev/null
+++ b/gui.theme/scroller.slide.v.tga
Binary files differ
diff --git a/gui.theme/select.arrow.down.tga b/gui.theme/select.arrow.down.tga
new file mode 100644
index 0000000..d721002
--- /dev/null
+++ b/gui.theme/select.arrow.down.tga
Binary files differ
diff --git a/gui.theme/select.arrow.hover.tga b/gui.theme/select.arrow.hover.tga
new file mode 100644
index 0000000..162d8e7
--- /dev/null
+++ b/gui.theme/select.arrow.hover.tga
Binary files differ
diff --git a/gui.theme/select.arrow.normal.tga b/gui.theme/select.arrow.normal.tga
new file mode 100644
index 0000000..162d8e7
--- /dev/null
+++ b/gui.theme/select.arrow.normal.tga
Binary files differ
diff --git a/gui.theme/select.arrow.png b/gui.theme/select.arrow.png
new file mode 100644
index 0000000..19de760
--- /dev/null
+++ b/gui.theme/select.arrow.png
Binary files differ
diff --git a/gui.theme/select.option.hover.png b/gui.theme/select.option.hover.png
new file mode 100644
index 0000000..fd9dc21
--- /dev/null
+++ b/gui.theme/select.option.hover.png
Binary files differ
diff --git a/gui.theme/select.option.normal.png b/gui.theme/select.option.normal.png
new file mode 100644
index 0000000..627790d
--- /dev/null
+++ b/gui.theme/select.option.normal.png
Binary files differ
diff --git a/gui.theme/select.options.png b/gui.theme/select.options.png
new file mode 100644
index 0000000..477a826
--- /dev/null
+++ b/gui.theme/select.options.png
Binary files differ
diff --git a/gui.theme/select.selected.down.tga b/gui.theme/select.selected.down.tga
new file mode 100644
index 0000000..7d952a0
--- /dev/null
+++ b/gui.theme/select.selected.down.tga
Binary files differ
diff --git a/gui.theme/select.selected.hover.tga b/gui.theme/select.selected.hover.tga
new file mode 100644
index 0000000..91dd794
--- /dev/null
+++ b/gui.theme/select.selected.hover.tga
Binary files differ
diff --git a/gui.theme/select.selected.normal.tga b/gui.theme/select.selected.normal.tga
new file mode 100644
index 0000000..54b8927
--- /dev/null
+++ b/gui.theme/select.selected.normal.tga
Binary files differ
diff --git a/gui.theme/slider.bar.hover.tga b/gui.theme/slider.bar.hover.tga
new file mode 100644
index 0000000..5e5c53a
--- /dev/null
+++ b/gui.theme/slider.bar.hover.tga
Binary files differ
diff --git a/gui.theme/slider.bar.normal.tga b/gui.theme/slider.bar.normal.tga
new file mode 100644
index 0000000..e9371c7
--- /dev/null
+++ b/gui.theme/slider.bar.normal.tga
Binary files differ
diff --git a/gui.theme/slider.tga b/gui.theme/slider.tga
new file mode 100644
index 0000000..ff3b4b2
--- /dev/null
+++ b/gui.theme/slider.tga
Binary files differ
diff --git a/gui.theme/tool.down.tga b/gui.theme/tool.down.tga
new file mode 100644
index 0000000..f89d4b4
--- /dev/null
+++ b/gui.theme/tool.down.tga
Binary files differ
diff --git a/gui.theme/tool.hover.tga b/gui.theme/tool.hover.tga
new file mode 100644
index 0000000..184ae9c
--- /dev/null
+++ b/gui.theme/tool.hover.tga
Binary files differ
diff --git a/gui.theme/tool.normal.tga b/gui.theme/tool.normal.tga
new file mode 100644
index 0000000..b304b87
--- /dev/null
+++ b/gui.theme/tool.normal.tga
Binary files differ
diff --git a/gui.theme/up.png b/gui.theme/up.png
new file mode 100644
index 0000000..d42c324
--- /dev/null
+++ b/gui.theme/up.png
Binary files differ
diff --git a/gui.theme/vbox.normal.png b/gui.theme/vbox.normal.png
new file mode 100644
index 0000000..9229c87
--- /dev/null
+++ b/gui.theme/vbox.normal.png
Binary files differ
diff --git a/gui.theme/vdot.down.png b/gui.theme/vdot.down.png
new file mode 100644
index 0000000..e9e781e
--- /dev/null
+++ b/gui.theme/vdot.down.png
Binary files differ
diff --git a/gui.theme/vdot.hover.png b/gui.theme/vdot.hover.png
new file mode 100644
index 0000000..74e043b
--- /dev/null
+++ b/gui.theme/vdot.hover.png
Binary files differ
diff --git a/gui.theme/vdot.normal.png b/gui.theme/vdot.normal.png
new file mode 100644
index 0000000..f64089b
--- /dev/null
+++ b/gui.theme/vdot.normal.png
Binary files differ
diff --git a/gui.theme/vsbox.normal.png b/gui.theme/vsbox.normal.png
new file mode 100644
index 0000000..2deca17
--- /dev/null
+++ b/gui.theme/vsbox.normal.png
Binary files differ
diff --git a/gui.theme/vslider.bar.hover.tga b/gui.theme/vslider.bar.hover.tga
new file mode 100644
index 0000000..fd1bde9
--- /dev/null
+++ b/gui.theme/vslider.bar.hover.tga
Binary files differ
diff --git a/gui.theme/vslider.bar.normal.tga b/gui.theme/vslider.bar.normal.tga
new file mode 100644
index 0000000..07ee06e
--- /dev/null
+++ b/gui.theme/vslider.bar.normal.tga
Binary files differ
diff --git a/gui.theme/vslider.down.tga b/gui.theme/vslider.down.tga
new file mode 100644
index 0000000..61c75a6
--- /dev/null
+++ b/gui.theme/vslider.down.tga
Binary files differ
diff --git a/gui.theme/vslider.tga b/gui.theme/vslider.tga
new file mode 100644
index 0000000..ff3b4b2
--- /dev/null
+++ b/gui.theme/vslider.tga
Binary files differ
diff --git a/gui.theme/vslider.up.tga b/gui.theme/vslider.up.tga
new file mode 100644
index 0000000..ce73c30
--- /dev/null
+++ b/gui.theme/vslider.up.tga
Binary files differ
diff --git a/gui.theme/x.png b/gui.theme/x.png
new file mode 100644
index 0000000..d00f36b
--- /dev/null
+++ b/gui.theme/x.png
Binary files differ
diff --git a/mxtheme.py b/mxtheme.py
new file mode 100644
index 0000000..d2cab13
--- /dev/null
+++ b/mxtheme.py
@@ -0,0 +1,25 @@
+"""Theme definition for Conozco Mexico"""
+
+theme_list = ['states', 'capitals']
+
+theme_defs = {
+ 'states': {
+ 'background': 'mexico_states_long.png',
+ 'icon': 'state_icon.png',
+ 'zones':'zones_states.txt',
+ 'levels':'levels_states.txt',
+ 'zonepic':'zones_states.png',
+ 'buttons': [
+ ]
+ },
+ 'capitals': {
+ 'background': 'mexico_capitals_long.png',
+ 'icon': 'capital_icon.png',
+ 'zones':'zones_capitals.txt',
+ 'levels':'levels_capitals.txt',
+ 'zonepic': 'zones_capitals.png',
+ 'buttons': [
+ ]
+ },
+ }
+
diff --git a/olpcgames/COPYING b/olpcgames/COPYING
new file mode 100755
index 0000000..b8adee0
--- /dev/null
+++ b/olpcgames/COPYING
@@ -0,0 +1,24 @@
+* Copyright (c) 2007, One Laptop Per Child.
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions are met:
+* * Redistributions of source code must retain the above copyright
+* notice, this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above copyright
+* notice, this list of conditions and the following disclaimer in the
+* documentation and/or other materials provided with the distribution.
+* * Neither the name of One Laptop Per Child nor the
+* names of its contributors may be used to endorse or promote products
+* derived from this software without specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY ONE LAPTOP PER CHILD ``AS IS'' AND ANY
+* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+* DISCLAIMED. IN NO EVENT SHALL ONE LAPTOP PER CHILD BE LIABLE FOR ANY
+* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/olpcgames/__init__.py b/olpcgames/__init__.py
new file mode 100755
index 0000000..504388c
--- /dev/null
+++ b/olpcgames/__init__.py
@@ -0,0 +1,102 @@
+"""Wrapper/adaptation system for writing/porting Pygame games to OLPC/Sugar
+
+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
+e.g. the Camera and Mesh Network system.
+
+Considerations for Developers:
+
+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
+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.
+
+Attributes of Note:
+
+ ACTIVITY -- if not None, then the activity instance which represents
+ this activity at the Sugar shell level.
+ 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,
+)
+
+try:
+ from olpcgames.activity import PygameActivity as PyGameActivity
+except ImportError, err:
+ PyGameActivity = None
+
diff --git a/olpcgames/_cairoimage.py b/olpcgames/_cairoimage.py
new file mode 100755
index 0000000..3cfa22c
--- /dev/null
+++ b/olpcgames/_cairoimage.py
@@ -0,0 +1,135 @@
+"""Utility functions for cairo-specific operations
+
+USE_BASE_ARRAY -- if False (default), uses numpy arrays,
+ currently this is the only version that works on 32-bit
+ machines.
+"""
+import pygame, struct, logging
+big_endian = struct.pack( '=i', 1 ) == struct.pack( '>i', 1 )
+
+log = logging.getLogger( 'olpcgames._cairoimage' )
+##log.setLevel( logging.DEBUG )
+
+USE_BASE_ARRAY = False
+
+def newContext( width, height ):
+ """Create a new render-to-image context
+
+ width, height -- pixel dimensions to be rendered
+
+ Produces an ARGB format Cairo ImageSurface for
+ rendering your data into using rsvg, Cairo or Pango.
+
+ returns (ImageSurface, CairoContext) for rendering
+ """
+ import cairo
+ csrf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ context = cairo.Context (csrf)
+ #log.info( 'Format (expect: %s): %s', cairo.FORMAT_ARGB32, csrf.get_format())
+ return csrf, context
+
+def mangle_color(color):
+ """Mange a colour depending on endian-ness, and swap-necessity
+
+ Converts a 3 or 4 int (or float) value in the range 0-255 into a
+ 4-float value in the range 0.0-1.0
+ """
+ r,g,b = color[:3]
+ if len(color) > 3:
+ a = color[3]
+ else:
+ a = 255.0
+ return map(_fixColorBase, (r,g,b,a) )
+
+def _fixColorBase( v ):
+ """Return a properly clamped colour in floating-point space"""
+ 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.
+ """
+ # 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'
+ 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),
+ format
+ ) # there's the next
+ except ValueError, err:
+ err.args += (len(data), (width,height), width*height*4,format )
+ raise
+
+if __name__ == "__main__":
+ import unittest
+ logging.basicConfig()
+ class Tests( unittest.TestCase ):
+ def test_colours( self ):
+ """Test that colours are correctly translated
+
+ If we draw a given colour in cairo, we want the same
+ colour to show up in Pygame, let's test that...
+ """
+ for sourceColour in [
+ (255,0,0, 255),
+ (0,255,0, 255),
+ (0,0,255, 255),
+ (255,255,0, 255),
+ (0,255,255,255),
+ (255,0,255,255),
+ ]:
+ csrf,cctx = newContext( 1,1 )
+ background = mangle_color( sourceColour )
+ cctx.set_source_rgba(*background)
+ cctx.paint()
+ img = asImage( csrf )
+ colour = img.get_at( (0,0))
+ assert colour == sourceColour, (sourceColour,mangle_color(sourceColour),colour)
+ unittest.main()
diff --git a/olpcgames/_gtkmain.py b/olpcgames/_gtkmain.py
new file mode 100755
index 0000000..33a6a83
--- /dev/null
+++ b/olpcgames/_gtkmain.py
@@ -0,0 +1,70 @@
+"""Support for GObject mainloop-requiring libraries when not inside GTK
+
+INITIALIZED -- whether we have a running gobject loop yet...
+LOOP_TRACKER -- if present, the manual gtk event loop used to
+ support gobject-based code running in a non-Gobject event loop
+
+Holder -- objects which can be held as attributes to keep the mainloop running
+"""
+import threading, logging
+log = logging.getLogger( 'olpcgames._gtkmain' )
+##log.setLevel( logging.DEBUG )
+
+INITIALIZED = False
+LOOP_TRACKER = None
+
+class _TrackLoop( object ):
+ """Tracks the number of open loops and stops when finished"""
+ count = 0
+ _mainloop = None
+ def increment( self ):
+ log.info( 'Increment from %s', self.count )
+ self.count += 1 # XXX race condition here?
+ if self.count == 1:
+ log.info( 'Creating GObject mainloop')
+ self.t_loop = threading.Thread(target=self.loop)
+ self.t_loop.setDaemon( True )
+ self.t_loop.start()
+ def decrement( self ):
+ log.info( 'Decrement from %s', self.count )
+ self.count -= 1
+ def loop( self ):
+ """Little thread loop that replicates the gtk mainloop"""
+ import gtk
+ while self.count >= 1:
+ log.debug( 'GTK loop restarting' )
+ while gtk.events_pending():
+ gtk.main_iteration()
+ log.debug( 'GTK loop exiting' )
+ try:
+ del self.t_loop
+ except AttributeError, err:
+ pass
+
+class Holder():
+ """Object which, while held, keeps the gtk mainloop running"""
+ def __init__( self ):
+ log.info( 'Beginning hold on GTK mainloop with Holder object' )
+ startGTK()
+ def __del__( self ):
+ log.info( 'Releasing hold on GTK mainloop with Holder object' )
+ stopGTK()
+
+def startGTK( ):
+ """GTK support is required here, process..."""
+ if not INITIALIZED:
+ init()
+ if LOOP_TRACKER:
+ LOOP_TRACKER.increment()
+def stopGTK( ):
+ """GTK support is no longer required, release"""
+ if LOOP_TRACKER:
+ LOOP_TRACKER.decrement()
+def init( ):
+ """Create a gobject mainloop in a sub-thread (you don't need to call this normally)"""
+ global INITIALIZED, LOOP_TRACKER
+ if not INITIALIZED:
+ if not LOOP_TRACKER:
+ LOOP_TRACKER = _TrackLoop()
+ INITIALIZED = True
+ return LOOP_TRACKER
diff --git a/olpcgames/_version.py b/olpcgames/_version.py
new file mode 100755
index 0000000..6a4e1db
--- /dev/null
+++ b/olpcgames/_version.py
@@ -0,0 +1,2 @@
+"""Module defining the current version of the library"""
+__version__ = '1.6'
diff --git a/olpcgames/activity.py b/olpcgames/activity.py
new file mode 100755
index 0000000..d4a2b5a
--- /dev/null
+++ b/olpcgames/activity.py
@@ -0,0 +1,241 @@
+"""Embeds the Canvas widget into a Sugar-specific Activity environment
+
+The olpcgames.activity module encapsulates creation of a Pygame activity.
+Your Activity should inherit from this class. Simply setting some class
+attributes is all you need to do in a class inheriting from
+olpcgames.activity.PygameActivity in order to get Pygame to work.
+
+(The skeleton builder script creates this file automatically for you).
+
+Note:
+ You should not import pygame into your activity file, as the olpcgames
+ wrapper needs to be initialized before pygame is imported the first time.
+
+Example usage:
+
+ class PygameActivity(activity.Activity):
+ game_name = None
+ game_title = 'Pygame Game'
+ game_size = (units.grid_to_pixels(16),
+ units.grid_to_pixels(11))
+ pygame_mode = 'SDL'
+"""
+import logging
+logging.root.setLevel( logging.WARN )
+log = logging.getLogger( 'olpcgames.activity' )
+##log.setLevel( logging.DEBUG )
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gtk.gdk
+import os
+
+from sugar.activity import activity
+from sugar.graphics import style
+from olpcgames.canvas import PygameCanvas
+from olpcgames import mesh, util
+
+__all__ = ['PygameActivity']
+
+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.
+
+ 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
+ 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,
+ '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
+ if you want to provide a different canvas class, e.g. to provide a different
+ internal layout. Note: only used where pygame_mode == 'SDL'
+
+ The Activity, once created, will be made available as olpcgames.ACTIVITY,
+ and that access mechanism should allow code to test for the presence of the
+ activity before accessing Sugar-specific functionality.
+
+ XXX Note that currently the toolbar and window layout are hard-coded into
+ this super-class, with no easy way of overriding without completely rewriting
+ the __init__ method. We should allow for customising both the UI layout and
+ the toolbar contents/layout/connection.
+
+ XXX Note that if you change the title of your activity in the toolbar you may
+ see the same focus issues as we have patched around in the build_toolbar
+ method. If so, please report them to Mike Fletcher.
+ """
+ game_name = None
+ game_title = 'Pygame Game'
+ game_handler = None
+ game_size = (16 * style.GRID_CELL_SIZE,
+ 11 * style.GRID_CELL_SIZE)
+ pygame_mode = 'SDL'
+
+ def __init__(self, handle):
+ """Initialise the Activity with the activity-description handle"""
+ super(PygameActivity, self).__init__(handle)
+ self.make_global()
+ if self.game_size is None:
+ width,height = gtk.gdk.screen_width(), gtk.gdk.screen_height()
+ log.info( 'Total screen size: %s %s', width,height)
+ # 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())
+ 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
+ """
+ import weakref, olpcgames
+ assert not olpcgames.ACTIVITY, """Activity.make_global called twice, have you created two Activity instances in a single process?"""
+ olpcgames.ACTIVITY = weakref.proxy( self )
+
+ def build_toolbar( self ):
+ """Build our Activity toolbar for the Sugar system
+
+ This is a customisation point for those games which want to
+ provide custom toolbars when running under Sugar.
+ """
+ toolbar = activity.ActivityToolbar(self)
+ toolbar.show()
+ self.set_toolbox(toolbar)
+ def shared_cb(*args, **kwargs):
+ log.info( 'shared: %s, %s', args, kwargs )
+ try:
+ mesh.activity_shared(self)
+ except Exception, err:
+ log.error( """Failure signaling activity sharing to mesh module: %s""", util.get_traceback(err) )
+ else:
+ log.info( 'mesh activity shared message sent, trying to grab focus' )
+ try:
+ self._pgc.grab_focus()
+ except Exception, err:
+ log.warn( 'Focus failed: %s', err )
+ else:
+ log.info( 'asserting focus' )
+ assert self._pgc.is_focus(), """Did not successfully set pygame canvas focus"""
+ log.info( 'callback finished' )
+
+ def joined_cb(*args, **kwargs):
+ log.info( 'joined: %s, %s', args, kwargs )
+ mesh.activity_joined(self)
+ self._pgc.grab_focus()
+ self.connect("shared", shared_cb)
+ self.connect("joined", joined_cb)
+
+ if self.get_shared():
+ # if set at this point, it means we've already joined (i.e.,
+ # launched from Neighborhood)
+ joined_cb()
+
+ toolbar.title.unset_flags(gtk.CAN_FOCUS)
+ return toolbar
+
+ PYGAME_CANVAS_CLASS = PygameCanvas
+ def build_canvas( self ):
+ """Construct the Pygame or PygameCairo canvas for drawing"""
+ assert self.game_handler or self.game_name, 'You must specify a game_handler or game_name on your Activity (%r)'%(
+ self.game_handler or self.game_name
+ )
+ if self.pygame_mode != 'Cairo':
+ self._pgc = self.PYGAME_CANVAS_CLASS(*self.game_size)
+ self.set_canvas(self._pgc)
+ self._pgc.grab_focus()
+ self._pgc.connect_game(self.game_handler or self.game_name)
+ # XXX Bad coder, do not hide in a widely subclassed operation
+ # map signal does not appear to show up on socket instances
+ gtk.gdk.threads_init()
+ return self._pgc
+ else:
+ import hippo
+ self._drawarea = gtk.DrawingArea()
+ canvas = hippo.Canvas()
+ canvas.grab_focus()
+ self.set_canvas(canvas)
+ self.show_all()
+
+ import pygamecairo
+ pygamecairo.install()
+
+ pygamecairo.display.init(canvas)
+ app = self.game_handler or self.game_name
+ if ':' not in app:
+ app += ':main'
+ mod_name, fn_name = app.split(':')
+ mod = __import__(mod_name, globals(), locals(), [])
+ fn = getattr(mod, fn_name)
+ fn()
+ def read_file(self, file_path):
+ """Handle request to read the given file on the Pygame side
+
+ This is complicated rather noticeably by the silly semantics of the Journal
+ where it unlinks the file as soon as this method returns. We either have to
+ handle the file-opening in PyGTK (not acceptable), block this thread until
+ the Pygame thread handles the event (which it may never do) or we have
+ to make the silly thing use a non-standard file-opening interface.
+ """
+ log.info( 'read_file: %s %s', file_path, self.metadata )
+ import olpcgames, pygame
+ from olpcgames import eventwrap
+ event = eventwrap.Event(
+ type = pygame.USEREVENT,
+ code = olpcgames.FILE_READ_REQUEST,
+ filename = file_path,
+ metadata = self.metadata,
+ )
+ eventwrap.post( event )
+ event.block()
+ def write_file( self, file_path ):
+ """Handle request to write to the given file on the Pygame side
+
+ This is rather complicated by the need to have the file complete by the
+ time the function returns. Very poor API, after all, if I have to write a
+ multi-hundred-megabyte file it might take many minutes to complete
+ writing.
+ """
+ log.info( 'write_file: %s %s', file_path, self.metadata )
+ if os.path.exists( file_path ):
+ self.read_file( file_path )
+ import olpcgames, pygame
+ from olpcgames import eventwrap
+ event = eventwrap.Event(
+ type = pygame.USEREVENT,
+ code = olpcgames.FILE_WRITE_REQUEST,
+ filename = file_path,
+ metadata = self.metadata,
+ )
+ eventwrap.post( event )
+ event.block()
+ if not os.path.exists( file_path ):
+ log.warn( '''No file created in %r''', file_path )
+ raise NotImplementedError( """Pygame Activity code did not produce a file for %s"""%( file_path, ))
+ else:
+ log.info( '''Stored file in %r''', file_path )
+
+
+import olpcgames
+olpcgames.PyGameActivity = PygameActivity
+PyGameActivity = PygameActivity
diff --git a/olpcgames/buildmanifest.py b/olpcgames/buildmanifest.py
new file mode 100755
index 0000000..899433b
--- /dev/null
+++ b/olpcgames/buildmanifest.py
@@ -0,0 +1,33 @@
+#! /usr/bin/env python
+"""Stupid little script to automate generation of MANIFEST and po/POTFILES.in
+
+Really this should have been handled by using distutils, but oh well,
+distutils is a hoary beast and I can't fault people for not wanting to
+spend days spelunking around inside it to find the solutions...
+"""
+from distutils.filelist import FileList
+import os
+
+def fileList( template ):
+ """Produce a formatted file-list for storing in a file"""
+ files = FileList()
+ for line in filter(None,template.splitlines()):
+ files.process_template_line( line )
+ content = '\n'.join( files.files )
+ return content
+
+
+def main( ):
+ """Do the quicky finding of files for our manifests"""
+ content = fileList( open('MANIFEST.in').read() )
+ open( 'MANIFEST','w').write( content )
+
+ content = fileList( open('POTFILES.in').read() )
+ try:
+ os.makedirs( 'po' )
+ except OSError, err:
+ pass
+ open( os.path.join('po','POTFILES.in'), 'w').write( content )
+
+if __name__ == "__main__":
+ main()
diff --git a/olpcgames/camera.py b/olpcgames/camera.py
new file mode 100755
index 0000000..249f295
--- /dev/null
+++ b/olpcgames/camera.py
@@ -0,0 +1,221 @@
+"""Accesses OLPC Camera functionality via gstreamer
+
+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
+
+
+"""
+import threading, subprocess
+import logging
+import olpcgames
+import time
+import os
+import pygame
+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
+
+class Camera(object):
+ """A class representing a still-picture camera
+
+ Produces a simple gstreamer bus that terminates in a filesink, that is,
+ it stores the results in a file. When a picture is "snapped" the gstreamer
+ stream is iterated until it finishes processing and then the file can be
+ read.
+
+ There are two APIs available, a synchronous API which can potentially
+ stall your activity's GUI (and is NOT recommended) and an
+ asynchronous API which returns immediately and delivers the captured
+ camera image via a Pygame event. To be clear, it is recommended
+ that you use the snap_async method, *not* the snap method.
+
+ Note:
+
+ The Camera class is simply a convenience wrapper around a fairly
+ straightforward gst-launch bus. If you have more involved
+ requirements for your camera manipulations you will probably
+ find it easier to write your own camera implementation than to
+ use this one. Basically we provide here the "normal" use case of
+ snapping a picture into a pygame image.
+
+ Note:
+
+ With the current camera implementation taking a single photograph
+ requires about 6 seconds! Obviously we'll need to figure out what's
+ taking gstreamer so long to process the pipe and fix that.
+
+ """
+ _aliases = {
+ 'camera': 'v4l2src',
+ 'test': 'videotestsrc',
+ 'testing': 'videotestsrc',
+ 'png': 'pngenc',
+ 'jpeg': 'jpegenc',
+ 'jpg': 'jpegenc',
+ }
+ def __init__(self, source='camera', format='png', filename=None, directory = None):
+ """Initialises the Camera's internal description
+
+ source -- the gstreamer source for the video to capture, useful values:
+ 'v4l2src','camera' -- the camera
+ 'videotestsrc','test' -- test pattern generator source
+ 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
+ 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"""
+ if not self.directory:
+ path = os.path.join( get_activity_root(), 'tmp' )
+ try:
+ os.makedirs( path )
+ log.info( 'Created temporary directory: %s', path )
+ except (OSError,IOError), err:
+ pass
+ else:
+ path = self.directory
+ 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
+ )
+
+ 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!
+ """
+ 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() )
+ def _load_and_clean( self, filename ):
+ """Use pygame to load given filename, delete after loading/attempt"""
+ try:
+ log.info( 'Loading snapshot file: %s', filename )
+ return pygame.image.load(filename)
+ finally:
+ try:
+ os.remove( filename )
+ except (IOError,OSError), err:
+ pass
+ def snap_async( self, token=None ):
+ """Snap a picture asynchronously generating event on success/failure
+
+ 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"
+ 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).
+ 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 )
+ """
+ log.debug( 'beginning async snap')
+ t = threading.Thread(target=self._background_snap, args=[token])
+ t.start()
+ return token
+
+ def _background_snap(
+ self,
+ token = None,
+ ):
+ """Process gst messages until pipe is finished
+
+ pipe -- gstreamer pipe definition for parse_launch, normally it will
+ produce a file into which the camera should store an image
+
+ We consider pipe to be finished when we have had two "state changed"
+ gstreamer events where the pending state is VOID, the first for when
+ 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 )
+
+def snap():
+ """Dump a snapshot from the camera to a pygame surface in background thread
+
+ See Camera.snap
+ """
+ return Camera().snap()
+
+def snap_async( token=None, **named ):
+ """Dump snapshot from camera return asynchronously as event in Pygame
+
+ See Camera.snap_async
+ """
+ return Camera(**named).snap_async( token )
diff --git a/olpcgames/canvas.py b/olpcgames/canvas.py
new file mode 100755
index 0000000..2583827
--- /dev/null
+++ b/olpcgames/canvas.py
@@ -0,0 +1,171 @@
+"""Implements bridge connection between Sugar/GTK and Pygame"""
+import os
+import sys
+import logging
+log = logging.getLogger( 'olpcgames.canvas' )
+##log.setLevel( logging.DEBUG )
+import threading
+from pprint import pprint
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+import pygame
+
+from olpcgames import gtkEvent, util
+
+__all__ = ['PygameCanvas']
+
+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
+ 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
+ will be writing to the Window ID of the socket. The internal EventBox is
+ 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__()
+ self.set_flags(gtk.CAN_FOCUS)
+
+ # Build the sub-widgets
+ self._align = gtk.Alignment(0.5, 0.5)
+ self._inner_evb = gtk.EventBox()
+ self._socket = gtk.Socket()
+
+
+ # Add internal widgets
+ self._inner_evb.set_size_request(width, height)
+ self._inner_evb.add(self._socket)
+
+ self._socket.show()
+
+ self._align.add(self._inner_evb)
+ self._inner_evb.show()
+
+ self._align.show()
+
+ self.put(self._align, 0,0)
+
+ # Construct a gtkEvent.Translator
+ self._translator = gtkEvent.Translator(self, self._inner_evb)
+ # <Cue Thus Spract Zarathustra>
+ self.show()
+ def connect_game(self, app):
+ """Imports the given main-loop and starts processing in secondary thread
+
+ app -- fully-qualified Python path-name for the game's main-loop, with
+ name within module as :functionname, if no : character is present then
+ :main will be assumed.
+
+ 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.
+ """
+ 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']
+ pygame.init()
+
+ self._translator.hook_pygame()
+
+ # Load the modules
+ # NOTE: This is delayed because pygame.init() must come after the embedding is up
+ 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)
+
+ # Start Pygame
+ self.__thread = threading.Thread(target=self._start, args=[fn])
+ self.__thread.start()
+
+ def _start(self, fn):
+ """The method that actually runs in the background thread"""
+ log.info( 'Staring the mainloop' )
+ import olpcgames
+ olpcgames.widget = olpcgames.WIDGET = self
+ try:
+ import sugar.activity.activity,os
+ except ImportError, err:
+ log.info( """Running outside Sugar""" )
+ else:
+ try:
+ os.chdir(sugar.activity.activity.get_bundle_path())
+ except KeyError, err:
+ pass
+
+ 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()
+ 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
new file mode 100755
index 0000000..8510186
--- /dev/null
+++ b/olpcgames/data/__init__.py
@@ -0,0 +1,36 @@
+"""Design-time __init__.py for resourcepackage
+
+This is the scanning version of __init__.py for your
+resource modules. You replace it with a blank or doc-only
+init when ready to release.
+"""
+try:
+ __file__
+except NameError:
+ pass
+else:
+ import os
+ if os.path.splitext(os.path.basename( __file__ ))[0] == "__init__":
+ try:
+ from resourcepackage import package, defaultgenerators
+ generators = defaultgenerators.generators.copy()
+
+ ### CUSTOMISATION POINT
+ ## import specialised generators here, such as for wxPython
+ #from resourcepackage import wxgenerators
+ #generators.update( wxgenerators.generators )
+ except ImportError:
+ pass
+ else:
+ package = package.Package(
+ packageName = __name__,
+ directory = os.path.dirname( os.path.abspath(__file__) ),
+ generators = generators,
+ )
+ package.scan(
+ ### CUSTOMISATION POINT
+ ## force true -> always re-loads from external files, otherwise
+ ## only reloads if the file is newer than the generated .py file.
+ # force = 1,
+ )
+
diff --git a/olpcgames/data/sleeping_svg.py b/olpcgames/data/sleeping_svg.py
new file mode 100755
index 0000000..c52398a
--- /dev/null
+++ b/olpcgames/data/sleeping_svg.py
@@ -0,0 +1,61 @@
+# -*- coding: ISO-8859-1 -*-
+"""Resource sleeping_svg (from file sleeping.svg)"""
+# written by resourcepackage: (1, 0, 1)
+source = 'sleeping.svg'
+package = 'olpcgames.data'
+data = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\012<svg\012\
+ xmlns=\"http://www.w3.org/2000/svg\"\012 xmlns:xlink=\"http:/\
+/www.w3.org/1999/xlink\"\012 width=\"737\"\012 height=\"923\"\012 ve\
+rsion=\"1.0\">\012 <defs>\012 <linearGradient\012 id=\"linearG\
+radient3152\">\012 <stop\012 style=\"stop-color:#b8ffb4\
+;stop-opacity:1;\"\012 offset=\"0\" />\012 <stop\012 \
+ offset=\"0.5\"\012 style=\"stop-color:#2eff22;stop-opaci\
+ty:0.5;\" />\012 <stop\012 style=\"stop-color:#ffffff;s\
+top-opacity:0;\"\012 offset=\"1\" />\012 </linearGradient>\
+\012 <radialGradient\012 xlink:href=\"#linearGradient3152\"\
+\012 id=\"radialGradient3158\"\012 cx=\"260\"\012 cy=\"2\
+35\"\012 fx=\"260\"\012 fy=\"235\"\012 r=\"259\"\012 gr\
+adientTransform=\"matrix(1,0,0,1.2531846,0,-59.560934)\"\012 \
+ gradientUnits=\"userSpaceOnUse\" />\012 </defs>\012 <g\012 tran\
+sform=\"translate(-3,-73)\">\012 <path\012 style=\"opacity:1\
+;color:#000000;fill:url(#radialGradient3158);fill-opacity:1;\
+fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-lineca\
+p:butt;stroke-linejoin:miter;marker:none;marker-start:none;m\
+arker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-da\
+sharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility\
+:visible;display:inline;overflow:visible\"\012 id=\"path217\
+8\"\012 d=\"M 519 235 A 259 324 0 1 1 0,235 A 259 324 0 1 \
+1 519 235 z\"\012 transform=\"matrix(1.4203822,0,0,1.42038\
+22,0,200)\" />\012 <path\012 style=\"fill:#000000;fill-opac\
+ity:0.75;fill-rule:nonzero;stroke:none;stroke-width:1pt;stro\
+ke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\"\012 \
+ d=\"M 420,366 C 438,381 455,400 478,408 C 523,427 576,424 \
+620,405 C 632,400 644,393 655,387 C 652,389 638,397 649,391 \
+C 658,385 666,379 676,376 C 688,370 673,379 669,382 C 637,40\
+1 604,421 566,427 C 526,435 482,429 446,408 C 431,398 419,38\
+5 405,374 C 410,371 415,368 420,366 z \" />\012 <path\012 \
+style=\"fill:#000000;fill-opacity:0.75;fill-rule:nonzero;stro\
+ke:none;stroke-width:1pt;stroke-linecap:butt;stroke-linejoin\
+:miter;stroke-opacity:1\"\012 d=\"M 322,366 C 303,381 286,4\
+00 263,408 C 218,427 166,424 121,405 C 109,400 98,393 86,387\
+ C 89,389 103,397 93,391 C 84,385 75,379 65,376 C 53,370 68,\
+379 72,382 C 104,401 137,421 175,427 C 216,435 260,429 295,4\
+08 C 310,398 322,385 336,374 C 331,371 326,368 322,366 z \" /\
+>\012 <path\012 style=\"fill:#000000;fill-opacity:0.75;fil\
+l-rule:nonzero;stroke:none;stroke-width:1pt;stroke-linecap:b\
+utt;stroke-linejoin:miter;stroke-opacity:1\"\012 d=\"M 363,\
+383 C 347,418 353,458 345,495 C 339,525 324,551 312,579 C 30\
+4,598 298,620 309,639 C 317,655 335,667 353,669 C 379,671 40\
+5,664 429,653 C 442,646 405,667 423,656 C 429,652 434,647 44\
+1,645 C 455,639 439,650 434,653 C 408,669 378,679 347,679 C \
+327,679 308,667 297,651 C 285,634 287,613 294,594 C 302,570 \
+316,548 324,523 C 335,493 335,460 338,428 C 340,415 342,401 \
+349,390 C 353,388 358,385 363,383 z \" />\012 <path\012 st\
+yle=\"fill:#000000;fill-opacity:0.75;fill-rule:nonzero;stroke\
+:none;stroke-width:1pt;stroke-linecap:butt;stroke-linejoin:m\
+iter;stroke-opacity:1\"\012 d=\"M 206,735 C 245,737 285,740\
+ 324,744 C 357,745 391,746 424,744 C 468,738 510,723 550,703\
+ C 552,703 544,709 541,711 C 531,718 518,722 507,727 C 474,7\
+40 440,751 405,754 C 360,756 314,754 268,749 C 243,747 218,7\
+46 193,745 C 197,741 201,738 206,735 z \" />\012 </g>\012</svg>\012"
+### end
diff --git a/olpcgames/dbusproxy.py b/olpcgames/dbusproxy.py
new file mode 100755
index 0000000..a103e28
--- /dev/null
+++ b/olpcgames/dbusproxy.py
@@ -0,0 +1,93 @@
+"""Spike test for a safer networking system for DBUS-based objects"""
+from olpcgames import eventwrap, util
+from dbus import proxies
+import logging
+log = logging.getLogger( 'dbus' )
+log.setLevel( logging.DEBUG )
+
+def wrap( value, tube=None,path=None ):
+ """Wrap object with any required pygame-side proxies"""
+ if isinstance( value,proxies._ProxyMethod ):
+ return DBUSMethod( value, tube=tube, path=path )
+ elif isinstance( value, proxies._DeferredMethod ):
+ value._proxy_method = DBUSMethod( value._proxy_method, tube=tube, path=path )
+ return value
+ elif isinstance( value, proxies.ProxyObject ):
+ return DBUSProxy( value, tube=tube, path=path )
+ else:
+ return value
+
+class DBUSProxy( object ):
+ """Proxy for the DBUS Proxy object"""
+ def __init__( self, proxy, tube=None, path=None ):
+ log.info( 'Creating Pygame-side proxy for %s (%s)', proxy,path )
+ self.__proxy = proxy
+ self.__tube = tube
+ self.__path = path
+ def __getattr__( self, key ):
+ """Retrieve attribute of given key"""
+ from dbus import proxies
+ return wrap( getattr( self.__proxy, key ) )
+ def add_signal_receiver( self, callback, eventName, interface, path=None, sender_keyword='sender'):
+ """Add a new signal handler (which will be called many times) for given signal
+ """
+ log.info( """Setting signal receiver %s for event %s on interface %s (object path %s) with sender_keyword = %r""",
+ callback, eventName, interface, path, sender_keyword,
+ )
+ log.debug( """proxy: %s proxy.tube: %s""", self.__proxy, self.__proxy.tube )
+ self.__tube.add_signal_receiver(
+ Callback( callback ),
+ eventName,
+ interface,
+ path = path or self.__path,
+ sender_keyword = sender_keyword,
+ )
+
+class DBUSMethod( object ):
+ """DBUS method which does callbacks in the Pygame (eventwrapper) thread"""
+ def __init__( self, proxy, tube,path ):
+ log.info( 'Creating Pygame-side method proxy for %s', proxy )
+ self.__proxy = proxy
+ self.__tube = tube
+ self.__path = path
+ def __call__( self, *args, **named ):
+ """Perform the asynchronous call"""
+ log.info( 'Calling proxy for %s with *%s, **%s', self.__proxy, args, named )
+ callback, errback = named.get( 'reply_handler'), named.get( 'error_handler' )
+ if not callback:
+ raise TypeError( """Require a reply_handler named argument to do any asynchronous call""" )
+ else:
+ callback = Callback( callback )
+ if not errback:
+ errback = defaultErrback
+ else:
+ errback = Callback( errback )
+ named['reply_handler'] = callback
+ named['error_handler'] = errback
+ return self.__proxy( *args, **named )
+
+def defaultErrback( error ):
+ """Log the error to stderr/log"""
+ log.error( """Failure in DBUS call: %s""", error )
+
+class Callback( object ):
+ """PyGTK-side callback which generates a CallbackResult to process on the Pygame side"""
+ def __init__( self, callable, callContext = None):
+ """Initialize the callback to process results"""
+ self.callable = callable
+ if callContext is None:
+ callContext = util.get_traceback( None )
+ self.callContext = callContext
+ def __call__( self, *args, **named ):
+ """PyGTK-side callback operation"""
+ log.info( 'Callback %s return value *%s, **%s', self.callable, args, named )
+ from olpcgames import eventwrap
+ args = [wrap(a) for a in args]
+ named = dict([
+ (k,wrap(v)) for k,v in named.items()
+ ])
+ eventwrap.post(
+ eventwrap.CallbackResult(
+ self.callable, args, named, callContext = self.callContext
+ )
+ )
diff --git a/olpcgames/eventwrap.py b/olpcgames/eventwrap.py
new file mode 100755
index 0000000..402109c
--- /dev/null
+++ b/olpcgames/eventwrap.py
@@ -0,0 +1,388 @@
+"""Provides substitute for Pygame's "event" module using gtkEvent
+
+Provides methods which will be substituted into Pygame in order to
+provide the synthetic events that we will feed into the Pygame queue.
+These methods are registered by the "install" method.
+
+This event queue does not support getting events only of a certain type.
+You need to get all pending events at a time, or filter them yourself. You
+can, however, block and unblock events of certain types, so that may be
+useful to you.
+
+Set_grab doesn't do anything (you are not allowed to grab events). Sorry.
+
+Extensions:
+
+ wait( timeout=None ) -- allows you to wait for only a specified period
+ before you return to the application. Can be used to e.g. wait for a
+ short period, then release some resources, then wait a bit more, then
+ release a few more resources, then a bit more...
+"""
+import pygame
+import gtk
+import Queue
+import thread, threading
+import logging
+from olpcgames import util
+
+log = logging.getLogger( 'olpcgames.eventwrap' )
+
+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"""
+ 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
+
+def install():
+ """Installs this module (eventwrap) as an in-place replacement for the pygame.event module.
+
+ Use install() when you need to interact with Pygame code written
+ without reference to the olpcgames wrapper mechanisms to have the
+ code use this module's event queue.
+
+ XXX Really, use it everywhere you want to use olpcgames, as olpcgames
+ registers the handler itself, so you will always wind up with it registered when
+ you use olpcgames (the gtkEvent.Translator.hook_pygame method calls it).
+ """
+ log.info( 'Installing OLPCGames event wrapper' )
+ from olpcgames import eventwrap
+ import pygame
+ pygame.event = eventwrap
+ import sys
+ sys.modules["pygame.event"] = eventwrap
+
+# Event queue:
+class _FilterQueue( Queue.Queue ):
+ """Simple Queue sub-class with a put_left method"""
+ def get_type( self, filterFunction, block=True, timeout=None ):
+ """Get events of a given type
+
+ Note: can raise Empty *even* when blocking if someone else
+ pops the event off the queue before we get around to it.
+ """
+ self.not_empty.acquire()
+ try:
+ if not block:
+ if self._empty_type( filterFunction ):
+ raise Queue.Empty
+ elif timeout is None:
+ while self._empty_type( filterFunction ):
+ self.not_empty.wait()
+ else:
+ if timeout < 0:
+ raise ValueError("'timeout' must be a positive number")
+ endtime = _time() + timeout
+ while self._empty_type( filterFunction ):
+ remaining = endtime - _time()
+ if remaining <= 0.0:
+ raise Queue.Empty
+ self.not_empty.wait(remaining)
+ item = self._get_type( filterFunction )
+ self.not_full.notify()
+ return item
+ finally:
+ self.not_empty.release()
+ def _empty_type( self, filterFunction ):
+ """Are we empty with respect to filterFunction?"""
+ for element in self.queue:
+ if filterFunction( element ):
+ return False
+ return True
+ def _get_type( self, filterFunction ):
+ """Get the first instance which matches filterFunction"""
+ for element in self.queue:
+ if filterFunction( element ):
+ self.queue.remove( element )
+ return element
+ # someone popped the event off the queue before we got to it!
+ raise Queue.Empty
+ def peek_type( self, filterFunction= lambda x: True ):
+ """Peek to see if we have filterFunction-matching element
+
+ Note: obviously this is *not* thread safe, it's just informative...
+ """
+ try:
+ for element in self.queue:
+ if filterFunction( element ):
+ return element
+ return None
+ except RuntimeError, err:
+ return None # none yet, at least
+
+g_events = _FilterQueue()
+
+# Set of blocked events as set by set
+g_blocked = set()
+g_blockedlock = thread.allocate_lock() # should use threading instead
+g_blockAll = False
+
+def _typeChecker( types ):
+ """Create check whether an event is in types"""
+ try:
+ if 1 in types:
+ pass
+ def check( element ):
+ return element.type in types
+ return check
+ except TypeError, err:
+ def check( element ):
+ return element.type == types
+ return check
+
+def pump():
+ """Handle any window manager and other external events that aren't passed to the user
+
+ Call this periodically (once a frame) if you don't call get(), poll() or wait()
+ """
+ pygame_pump()
+ _releaseEvents()
+
+def get( types=None):
+ """Get a list of all pending events
+
+ types -- either an integer event-type or a sequence of integer event types
+ which restrict the set of event-types returned from the queue. Keep in mind
+ that if you do not remove events you may wind up with an eternally growing
+ queue or a full queue. Normally you will want to remove all events in your
+ top-level event-loop and propagate them yourself.
+
+ Note: if you use types you lose all event ordering guarantees, events
+ may show up after events which were originally produced before them due to
+ the re-ordering of the queue on filtering!
+ """
+ pump()
+ eventlist = []
+ try:
+ if types:
+ check = _typeChecker( types )
+ while True:
+ eventlist.append(g_events.get_type( check, block=False))
+ else:
+ while True:
+ eventlist.append(g_events.get(block=False))
+ except Queue.Empty:
+ pass
+
+ pygameEvents = pygame_get()
+ if pygameEvents:
+ log.info( 'Raw Pygame events: %s', pygameEvents)
+ eventlist.extend( pygameEvents )
+ return _recordEvents( eventlist )
+
+def poll():
+ """Get the next pending event if exists. Otherwise, return pygame.NOEVENT."""
+ pump()
+ try:
+ result = g_events.get(block=False)
+ return _recordEvents( [result] )[0]
+ except Queue.Empty:
+ return Event(pygame.NOEVENT)
+
+
+def wait( timeout = None):
+ """Get the next pending event, wait up to timeout if none
+
+ timeout -- if present, only wait up to timeout seconds, if we
+ do not find an event before then, return None. timeout
+ is an OLPCGames-specific extension.
+ """
+ pump()
+ try:
+ result = None
+ result = g_events.get(block=True, timeout=timeout)
+ try:
+ return _recordEvents( [result] )[0]
+ except IndexError, err:
+ return Event( type=pygame.NOEVENT )
+ 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
+ return not g_events.empty()
+
+def clear():
+ """Clears the entire pending queue of events
+
+ Rarely used
+ """
+ try:
+ discarded = []
+ while True:
+ discarded.append( g_events.get(block=False) )
+ discarded = _recordEvents( discarded )
+ _releaseEvents()
+ return discarded
+ except Queue.Empty:
+ pass
+
+def set_blocked(item):
+ """Block item/items from being added to the event queue"""
+ g_blockedlock.acquire()
+ try:
+ # FIXME: we do not currently know how to block all event types when
+ # you set_blocked(none).
+ [g_blocked.add(x) for x in makeseq(item)]
+ finally:
+ g_blockedlock.release()
+
+def set_allowed(item):
+ """Allow item/items to be added to the event queue"""
+ g_blockedlock.acquire()
+ try:
+ if item is None:
+ # Allow all events when you set_allowed(none). Strange, eh?
+ # Pygame is a wonderful API.
+ g_blocked.clear()
+ else:
+ [g_blocked.remove(x) for x in makeseq(item)]
+ finally:
+ g_blockedlock.release()
+
+def get_blocked(*args, **kwargs):
+ g_blockedlock.acquire()
+ try:
+ blocked = frozenset(g_blocked)
+ return blocked
+ finally:
+ g_blockedlock.release()
+
+def set_grab(grabbing):
+ """This method will not be implemented"""
+
+def get_grab():
+ """This method will not be implemented"""
+
+def post(event):
+ """Post a new event to the Queue of events"""
+ g_blockedlock.acquire()
+ try:
+ if getattr(event,'type',None) not in g_blocked:
+ g_events.put(event, block=False)
+ finally:
+ g_blockedlock.release()
+
+def makeseq(obj):
+ """Accept either a scalar object or a sequence, and return a sequence
+ over which we can iterate. If we were passed a sequence, return it
+ unchanged. If we were passed a scalar, return a tuple containing only
+ that scalar. This allows the caller to easily support one-or-many.
+ """
+ # Strings are the exception because you can iterate over their chars
+ # -- yet, for all the purposes I've ever cared about, I want to treat
+ # a string as a scalar.
+ if isinstance(obj, basestring):
+ return (obj,)
+ try:
+ # Except as noted above, if you can get an iter() from an object,
+ # it's a collection.
+ iter(obj)
+ return obj
+ except TypeError:
+ # obj is a scalar. Wrap it in a tuple so we can iterate over the
+ # one item.
+ return (obj,)
diff --git a/olpcgames/gtkEvent.py b/olpcgames/gtkEvent.py
new file mode 100755
index 0000000..6b20102
--- /dev/null
+++ b/olpcgames/gtkEvent.py
@@ -0,0 +1,289 @@
+"""gtkEvent.py: translate GTK events into Pygame events."""
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+import pygame
+from olpcgames import eventwrap
+import logging
+log = logging.getLogger( 'olpcgames.gtkevent' )
+##log.setLevel( logging.DEBUG )
+
+class _MockEvent(object):
+ """Used to inject key-repeat events on the gtk side."""
+ def __init__(self, keyval):
+ self.keyval = keyval
+
+class Translator(object):
+ """Utility class to translate GTK events into Pygame events
+
+ The Translator object interprets incoming GTK events and generates
+ Pygame events in the eventwrap module's queue as a result.
+ It also handles generating Pygame style key-repeat events
+ by synthesizing them via a GTK timer.
+ """
+ key_trans = {
+ 'Alt_L': pygame.K_LALT,
+ 'Alt_R': pygame.K_RALT,
+ 'Control_L': pygame.K_LCTRL,
+ 'Control_R': pygame.K_RCTRL,
+ 'Shift_L': pygame.K_LSHIFT,
+ 'Shift_R': pygame.K_RSHIFT,
+ 'Super_L': pygame.K_LSUPER,
+ 'Super_R': pygame.K_RSUPER,
+ 'KP_Page_Up' : pygame.K_KP9,
+ 'KP_Page_Down' : pygame.K_KP3,
+ 'KP_End' : pygame.K_KP1,
+ 'KP_Home' : pygame.K_KP7,
+ 'KP_Up' : pygame.K_KP8,
+ 'KP_Down' : pygame.K_KP2,
+ 'KP_Left' : pygame.K_KP4,
+ 'KP_Right' : pygame.K_KP6,
+
+ }
+
+ mod_map = {
+ pygame.K_LALT: pygame.KMOD_LALT,
+ pygame.K_RALT: pygame.KMOD_RALT,
+ pygame.K_LCTRL: pygame.KMOD_LCTRL,
+ pygame.K_RCTRL: pygame.KMOD_RCTRL,
+ pygame.K_LSHIFT: pygame.KMOD_LSHIFT,
+ pygame.K_RSHIFT: pygame.KMOD_RSHIFT,
+ }
+
+ def __init__(self, mainwindow, mouselistener=None):
+ """Initialise the Translator with the windows to which to listen"""
+ # _inner_evb is Mouselistener
+ self._mainwindow = mainwindow
+ if mouselistener is None:
+ mouselistener = mainwindow
+
+ self._inner_evb = mouselistener
+
+ # Need to set our X event masks so we see mouse motion and stuff --
+ mainwindow.set_events(
+ gtk.gdk.KEY_PRESS_MASK | \
+ gtk.gdk.KEY_RELEASE_MASK \
+ )
+
+ self._inner_evb.set_events(
+ gtk.gdk.POINTER_MOTION_MASK | \
+ gtk.gdk.POINTER_MOTION_HINT_MASK | \
+ gtk.gdk.BUTTON_MOTION_MASK | \
+ gtk.gdk.BUTTON_PRESS_MASK | \
+ gtk.gdk.BUTTON_RELEASE_MASK
+ )
+
+ # Callback functions to link the event systems
+ mainwindow.connect('unrealize', self._quit)
+ mainwindow.connect('key_press_event', self._keydown)
+ mainwindow.connect('key_release_event', self._keyup)
+ self._inner_evb.connect('button_press_event', self._mousedown)
+ self._inner_evb.connect('button_release_event', self._mouseup)
+ self._inner_evb.connect('motion-notify-event', self._mousemove)
+
+ # You might need to do this
+ mainwindow.set_flags(gtk.CAN_FOCUS)
+ self._inner_evb.set_flags(gtk.CAN_FOCUS)
+
+ # Internal data
+ self.__stopped = False
+ self.__keystate = [0] * 323
+ self.__button_state = [0,0,0]
+ self.__mouse_pos = (0,0)
+ self.__repeat = (None, None)
+ self.__held = set()
+ self.__held_time_left = {}
+ self.__held_last_time = {}
+ 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 )
+ 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
+ pygame.key.get_pressed = self._get_pressed
+ pygame.key.set_repeat = self._set_repeat
+ pygame.mouse.get_pressed = self._get_mouse_pressed
+ pygame.mouse.get_pos = self._get_mouse_pos
+ import eventwrap
+ eventwrap.install()
+
+ def _quit(self, data=None):
+ self.__stopped = True
+ eventwrap.post(eventwrap.Event(pygame.QUIT))
+
+ def _keydown(self, widget, event):
+ key = event.keyval
+ log.debug( 'key down: %s', key )
+ if key in self.__held:
+ return True
+ else:
+ if self.__repeat[0] is not None:
+ self.__held_last_time[key] = pygame.time.get_ticks()
+ self.__held_time_left[key] = self.__repeat[0]
+ self.__held.add(key)
+
+ return self._keyevent(widget, event, pygame.KEYDOWN)
+
+ def _keyup(self, widget, event):
+ key = event.keyval
+ if self.__repeat[0] is not None:
+ if key in self.__held:
+ # This is possibly false if set_repeat() is called with a key held
+ del self.__held_time_left[key]
+ del self.__held_last_time[key]
+ self.__held.discard(key)
+
+ return self._keyevent(widget, event, pygame.KEYUP)
+
+ def _keymods(self):
+ """Extract the keymods as they stand currently."""
+ mod = 0
+ for key_val, mod_val in self.mod_map.iteritems():
+ mod |= self.__keystate[key_val] and mod_val
+ return mod
+
+
+ def _keyevent(self, widget, event, type):
+ key = gtk.gdk.keyval_name(event.keyval)
+ if key is None:
+ # No idea what this key is.
+ return False
+
+ keycode = None
+ if key in self.key_trans:
+ keycode = self.key_trans[key]
+ elif hasattr(pygame, 'K_'+key.upper()):
+ 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
+
+ if keycode is not None:
+ if type == pygame.KEYDOWN:
+ mod = self._keymods()
+ self.__keystate[keycode] = type == pygame.KEYDOWN
+ if type == pygame.KEYUP:
+ mod = self._keymods()
+ ukey = unichr(gtk.gdk.keyval_to_unicode(event.keyval))
+ if ukey == '\000':
+ ukey = ''
+ evt = eventwrap.Event(type, key=keycode, unicode=ukey, mod=mod)
+ assert evt.key, evt
+ self._post(evt)
+ return True
+
+ def _get_pressed(self):
+ """Retrieve map/array of which keys are currently depressed (held down)"""
+ return self.__keystate
+
+ def _get_mouse_pressed(self):
+ """Return three-element array of which mouse-buttons are currently depressed (held down)"""
+ return self.__button_state
+
+ def _mousedown(self, widget, event):
+ self.__button_state[event.button-1] = 1
+ return self._mouseevent(widget, event, pygame.MOUSEBUTTONDOWN)
+
+ def _mouseup(self, widget, event):
+ self.__button_state[event.button-1] = 0
+ return self._mouseevent(widget, event, pygame.MOUSEBUTTONUP)
+
+ def _mouseevent(self, widget, event, type):
+
+ evt = eventwrap.Event(type,
+ button=event.button,
+ pos=(event.x, event.y))
+ self._post(evt)
+ return True
+
+ def _mousemove(self, widget, event):
+ # From http://www.learningpython.com/2006/07/25/writing-a-custom-widget-using-pygtk/
+ # if this is a hint, then let's get all the necessary
+ # information, if not it's all we need.
+ if event.is_hint:
+ x, y, state = event.window.get_pointer()
+ else:
+ x = event.x
+ y = event.y
+ state = event.state
+
+ rel = (x - self.__mouse_pos[0],
+ y - self.__mouse_pos[1])
+ self.__mouse_pos = (x, y)
+
+ self.__button_state = [
+ state & gtk.gdk.BUTTON1_MASK and 1 or 0,
+ state & gtk.gdk.BUTTON2_MASK and 1 or 0,
+ state & gtk.gdk.BUTTON3_MASK and 1 or 0,
+ ]
+
+ evt = eventwrap.Event(pygame.MOUSEMOTION,
+ pos=self.__mouse_pos,
+ rel=rel,
+ buttons=self.__button_state)
+ self._post(evt)
+ return True
+
+ def _tick(self):
+ """Generate synthetic events for held-down keys"""
+ cur_time = pygame.time.get_ticks()
+ for key in self.__held:
+ delta = cur_time - self.__held_last_time[key]
+ self.__held_last_time[key] = cur_time
+
+ self.__held_time_left[key] -= delta
+ if self.__held_time_left[key] <= 0:
+ self.__held_time_left[key] = self.__repeat[1]
+ self._keyevent(None, _MockEvent(key), pygame.KEYDOWN)
+
+ return True
+
+ def _set_repeat(self, delay=None, interval=None):
+ """Set the key-repetition frequency for held-down keys"""
+ if delay is not None and self.__repeat[0] is None:
+ self.__tick_id = gobject.timeout_add(10, self._tick)
+ elif delay is None and self.__repeat[0] is not None:
+ gobject.source_remove(self.__tick_id)
+ self.__repeat = (delay, interval)
+
+ def _get_mouse_pos(self):
+ """Retrieve the current mouse position as a two-tuple of integers"""
+ return self.__mouse_pos
+
+ def _post(self, evt):
+ try:
+ eventwrap.post(evt)
+ except pygame.error, e:
+ if str(e) == 'Event queue full':
+ print "Event queue full!"
+ pass
+ else:
+ raise e
diff --git a/olpcgames/mesh.py b/olpcgames/mesh.py
new file mode 100755
index 0000000..1ad4c43
--- /dev/null
+++ b/olpcgames/mesh.py
@@ -0,0 +1,583 @@
+'''Utilities for wrapping the telepathy network for Pygame
+
+The 'mesh' module allows your Pygame game to be Shared
+across the OLPC networking infrastructure (D-bus and Tubes).
+It offers a simplified view of the Telepathy system.
+
+All Sugar activities have a 'Share' menu (toolbar) which is
+intended to allow other people to join the activity instance
+and collaborate with you. When you select Share, the activity's
+icon appears on the Neighborhood view of other laptops.
+
+If you do nothing else with networking, this is all that will
+happen: if anyone selects your shared activity icon, they will
+just spawn a new instance of the activity, and they will get to
+play your game alone.
+
+The mesh module automatically sets up a connection from each
+participant to every other participant. It provides (string based)
+communications channels that let you either broadcast messages
+to other users or communicate point-to-point to one other user.
+
+You can use the "handles" which uniquely idenify users to send
+messages to an individual user (send_to( handle, message )) or
+broadcast( message ) to send a message to all participants.
+
+More advanced (structured) networking can be handled by using
+the get_object( handle, path ) function, which looks up an object
+(by DBUS path) shared by the user "handle" and returns a
+DBUS/Telepathy proxy for that object. The object you get back is
+actually an olpcgames.dbusproxy.DBUSProxy instance, which
+enforces asynchronous operations and runs your
+reply_handler/error_handler in the Pygame event loop.
+
+NOTE:
+ You *cannot* make synchronous calls on these objects!
+ You must use the named arguments:
+
+ reply_handler, error_handler
+
+ for every call which you perform on a shared object (normally
+ these are ExportedGObject instances).
+
+If you want to run your callbacks in the GTK event loop (for instance
+because they need to handle GTK-side objects), you can use the
+dbus_get_object function. This is *not* recommended for normal
+usage, as any call to Pygame operations within the GTK event loop
+can cause a segfault/core of your entire Activity.
+
+Note:
+
+ mesh sets up N**2 connections for each shared activity, obviously
+ that will not scale to very large shared activities.
+
+Note:
+
+ The intention is that mesh will be refactored, possibly as a
+ new module called "olpcgames.network", which would break out
+ the various components so that there is no longer an assumed
+ networking layout. We will attempt to retain the mesh module's
+ API as we do so.
+
+Events produced:
+
+ olpcgames.CONNECT -- The tube connection was started. (i.e., the
+ user clicked Share or started the activity from the Neighborhood
+ screen).
+
+ Event properties:
+
+ id -- a unique identifier for this connection. (shouldn't be needed
+ for anything)
+
+ olpcgames.PARTICIPANT_ADD -- A participant joined the activity.
+ This will trigger for the local user as well as any arriving remote
+ users. Note that this *only* occurs after the activity is shared,
+ that is, the local user does not appear until after they have
+ shared a locally-started activity.
+
+ Event properties:
+
+ handle -- the arriving user's handle (a uniquely identifying string
+ assigned to the user by the Telepathy system, not human
+ readable), see lookup_buddy to retrieve human-readable
+ descriptions of the user.
+
+ olpcgames.PARTICIPANT_REMOVE -- A participant quit the activity.
+
+ Event properties:
+
+ handle -- the departing user's handle.
+
+ olpcgames.MESSAGE_UNI -- A message was sent to you.
+
+ Event properties:
+
+ content -- the content of the message (a string)
+ handle -- the handle of the sending user.
+
+ olpcgames.MESSAGE_MULTI -- A message was sent to everyone.
+
+ Event properties:
+
+ content -- the content of the message (a string)
+ handle -- the handle of the sending user.
+
+Note:
+
+ Eventually we will stop using top-level Pygame event types for the
+ various networking message types (currently four of them). We will
+ likely use UserEvent with a sub-type specifier for the various events
+ that OLPCGames produces.
+
+See Also:
+
+ http://blog.vrplumber.com/2016 -- Discussion of how Productive uses
+ the mesh module and raw Telepathy (ExportedGObject instances)
+'''
+import logging
+log = logging.getLogger( 'olpcgames.mesh' )
+##log.setLevel( logging.DEBUG )
+import olpcgames
+from olpcgames.util import get_traceback
+try:
+ from sugar.presence.tubeconn import TubeConnection
+except ImportError, err:
+ TubeConnection = object
+try:
+ from dbus.gobject_service import ExportedGObject
+except ImportError, err:
+ ExportedGObject = object
+from dbus.service import method, signal
+
+try:
+ import telepathy
+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"""
+
+DBUS_IFACE="org.laptop.games.pygame"
+DBUS_PATH="/org/laptop/games/pygame"
+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
+
+
+# Private objects for useful purposes!
+pygametubes = []
+text_chan, tubes_chan = (None, None)
+conn = None
+initiating = False
+joining = False
+
+connect_callback = None
+
+def is_initiating():
+ '''A version of is_initiator that's a bit less goofy, and can be used
+ before the Tube comes up.'''
+ global initiating
+ return initiating
+
+def is_joining():
+ '''Returns True if the activity was started up by means of the
+ Neighbourhood mesh view.'''
+ global joining
+ return joining
+
+def set_connect_callback(cb):
+ '''Just the same as the Pygame event loop can listen for CONNECT,
+ this is just an ugly callback that the glib side can use to be aware
+ of when the Tube is ready.'''
+ global connect_callback
+ connect_callback = cb
+
+def activity_shared(activity):
+ '''Called when the user clicks Share.'''
+
+ global initiating
+ initiating = True
+
+ _setup(activity)
+
+
+ log.debug('This is my activity: making a tube...')
+ channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
+ if hasattr( channel, 'OfferDBusTube' ):
+ id = channel.OfferDBusTube(
+ DBUS_SERVICE, {})
+ else:
+ id = channel.OfferTube(
+ telepathy.TUBE_TYPE_DBUS, DBUS_SERVICE, {})
+
+ global connect_callback
+ if connect_callback is not None:
+ connect_callback()
+
+def activity_joined(activity):
+ '''Called at the startup of our Activity, when the user started it via Neighborhood intending to join an existing activity.'''
+
+ # Find out who's already in the shared activity:
+ log.debug('Joined an existing shared activity')
+
+ for buddy in activity._shared_activity.get_joined_buddies():
+ log.debug('Buddy %s is already in the activity' % buddy.props.nick)
+
+
+ global initiating
+ global joining
+ initiating = False
+ joining = True
+
+
+ _setup(activity)
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=_list_tubes_reply_cb,
+ error_handler=_list_tubes_error_cb)
+
+ global connect_callback
+ if connect_callback is not None:
+ connect_callback()
+
+def _getConn( activity ):
+ log.debug( '_getConn' )
+ 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
+
+
+
+def _setup(activity):
+ '''Determines text and tube channels for the current Activity. If no tube
+channel present, creates one. Updates text_chan and tubes_chan.
+
+setup(sugar.activity.Activity, telepathy.client.Connection)'''
+ global text_chan, tubes_chan, DBUS_SERVICE
+ 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"
+ if text_chan is None:
+ log.error("Presence service didn't create a text channel")
+ raise "Failure"
+
+ # Make sure we have a Tubes channel - PS doesn't yet provide one
+ if tubes_chan is None:
+ log.debug("Didn't find our Tubes channel, requesting one...")
+ tubes_chan = conn.request_channel(telepathy.CHANNEL_TYPE_TUBES,
+ telepathy.HANDLE_TYPE_ROOM, room, True)
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
+ new_tube_cb)
+
+ log.info( 'Setup for %s complete', activity )
+ return (text_chan, tubes_chan)
+
+def new_tube_cb(id, initiator, type, service, params, state):
+ log.debug("New_tube_cb called: %s %s %s" % (id, initiator, type))
+ if (type == telepathy.TUBE_TYPE_DBUS and service == DBUS_SERVICE):
+ if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
+ if hasattr( channel, 'AcceptDBusTube' ):
+ channel.AcceptDBusTube( id )
+ else:
+ channel.AcceptTube(id)
+
+ tube_conn = TubeConnection(conn,
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
+ id, group_iface=text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+
+ global pygametubes, initiating
+ pygametubes.append(PygameTube(tube_conn, initiating, len(pygametubes)))
+
+
+def _list_tubes_reply_cb(tubes):
+ for tube_info in tubes:
+ new_tube_cb(*tube_info)
+
+def _list_tubes_error_cb(e):
+ 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
+ """
+ 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)
+
+
+
+def get_buddy(dbus_handle):
+ """DEPRECATED: Get a Buddy from a handle
+
+ THIS API WAS NOT THREAD SAFE! It has been removed to avoid
+ extremely hard-to-debug failures in activities. Use lookup_buddy
+ instead!
+
+ Code that read:
+
+ get_buddy( handle )
+ doSomething( handle, buddy )
+ doSomethingElse( buddy )
+
+ Translates to:
+
+ def withBuddy( buddy ):
+ doSomething( handle, buddy )
+ doSomethingElse( buddy )
+ lookup_buddy( handle, callback=withBuddy )
+ """
+ raise RuntimeError(
+ """get_buddy is not thread safe and will crash your activity (hard). Use lookup_buddy."""
+ )
+
+def _get_presence_service( ):
+ """Attempt to retrieve the presence service (check for offline condition)
+
+ The presence service, when offline, has no preferred connection type,
+ so we check that before returning the object...
+ """
+ log.debug( """About to import sugar.presence.presenceservice""" )
+ 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 ))
+
+def instance(idx=0):
+ return pygametubes[idx]
+
+
+class PygameTube(ExportedGObject):
+ '''The object whose instance is shared across D-bus
+
+ Call instance() to get the instance of this object for your activity service.
+ Its 'tube' property contains the underlying D-bus Connection.
+ '''
+ def __init__(self, tube, is_initiator, tube_id):
+ super(PygameTube, self).__init__(tube, DBUS_PATH)
+ log.info( 'PygameTube init' )
+ self.tube = tube
+ self.is_initiator = is_initiator
+ self.entered = False
+ self.ordered_bus_names = []
+ PEvent.post(PEvent.Event(CONNECT, id=tube_id))
+
+ if not self.is_initiator:
+ self.tube.add_signal_receiver(self.new_participant_cb, 'NewParticipants', DBUS_IFACE, path=DBUS_PATH)
+ self.tube.watch_participants(self.participant_change_cb)
+ self.tube.add_signal_receiver(self.broadcast_cb, 'Broadcast', DBUS_IFACE, path=DBUS_PATH, sender_keyword='sender')
+
+
+ def participant_change_cb(self, added, removed):
+ log.debug( 'participant_change_cb: %s %s', added, removed )
+ for handle, bus_name in added:
+ dbus_handle = self.tube.participants[handle]
+ self.ordered_bus_names.append(dbus_handle)
+ PEvent.post(PEvent.Event(PARTICIPANT_ADD, handle=dbus_handle))
+
+ for handle in removed:
+ dbus_handle = self.tube.participants[handle]
+ self.ordered_bus_names.remove(dbus_handle)
+ PEvent.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle))
+
+ if self.is_initiator:
+ if not self.entered:
+ # Initiator will broadcast a new ordered_bus_names each time
+ # a participant joins.
+ self.ordered_bus_names = [self.tube.get_unique_name()]
+ self.NewParticipants(self.ordered_bus_names)
+
+ self.entered = True
+
+ @signal(dbus_interface=DBUS_IFACE, signature='as')
+ def NewParticipants(self, ordered_bus_names):
+ '''This is the NewParticipants signal, sent when the authoritative list of ordered_bus_names changes.'''
+ log.debug("sending NewParticipants: %s" % ordered_bus_names)
+ pass
+
+ @signal(dbus_interface=DBUS_IFACE, signature='s')
+ def Broadcast(self, content):
+ '''This is the Broadcast signal; it sends a message to all other activity participants.'''
+ pass
+
+ @method(dbus_interface=DBUS_IFACE, in_signature='s', out_signature='', sender_keyword='sender')
+ def Tell(self, content, sender=None):
+ '''This is the targeted-message interface; called when a message is received that was sent directly to me.'''
+ PEvent.post(PEvent.Event(MESSAGE_UNI, handle=sender, content=content))
+
+ def broadcast_cb(self, content, sender=None):
+ '''This is the Broadcast callback, fired when someone sends a Broadcast signal along the bus.'''
+ PEvent.post(PEvent.Event(MESSAGE_MULTI, handle=sender, content=content))
+
+ def new_participant_cb(self, new_bus_names):
+ '''This is the NewParticipants callback, fired when someone joins or leaves.'''
+ log.debug("new participant. new bus names %s, old %s" % (new_bus_names, self.ordered_bus_names))
+ if self.ordered_bus_names != new_bus_names:
+ log.warn("ordered bus names out of sync with server, resyncing")
+ self.ordered_bus_names = new_bus_names
+
+def send_to(handle, content=""):
+ '''Sends the given message to the given buddy identified by handle.'''
+ log.debug( 'send_to: %s %s', handle, content )
+ remote_proxy = dbus_get_object(handle, DBUS_PATH)
+ remote_proxy.Tell(content, reply_handler=dbus_msg, error_handler=dbus_err)
+
+def dbus_msg():
+ log.debug("async reply to send_to")
+def dbus_err(e):
+ log.error("async error: %s" % e)
+
+def broadcast(content=""):
+ '''Sends the given message to all participants.'''
+ log.debug( 'Broadcast: %s', content )
+ instance().Broadcast(content)
+
+def my_handle():
+ '''Returns the handle of this user
+
+ Note, you can get a DBusException from this if you have
+ not yet got a unique ID assigned by the bus. You may need
+ to delay calling until you are sure you are connected.
+ '''
+ log.debug( 'my handle' )
+ return instance().tube.get_unique_name()
+
+def is_initiator():
+ '''Returns the handle of this user.'''
+ log.debug( 'is initiator' )
+ return instance().is_initiator
+
+def get_participants():
+ '''Returns the list of active participants, in order of arrival.
+ List is maintained by the activity creator; if that person leaves it may not stay in sync.'''
+ log.debug( 'get_participants' )
+ try:
+ return instance().ordered_bus_names[:]
+ 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
+
+ This is how you can communicate with other participants using
+ arbitrary D-bus objects without having to manage the participants
+ yourself. You can use the returned proxy's methods from Pygame,
+ with your callbacks occuring in the Pygame thread, rather than
+ in the DBUS/GTK event loop.
+
+ Simply define a D-bus class with an interface and path that you
+ choose; when you want a reference to the corresponding remote
+ object on a participant, call this method.
+
+ returns an olpcgames.dbusproxy.DBUSProxy( ) object wrapping
+ the DBUSProxy object.
+
+ The dbus_get_object_proxy name is deprecated
+ '''
+ log.debug( 'DBUS get_object( %r %r )', handle, path )
+ from olpcgames import dbusproxy
+ return dbusproxy.DBUSProxy(
+ instance().tube.get_object( handle, path),
+ tube=instance().tube,
+ path=path
+ )
+
+dbus_get_object_proxy = get_object
diff --git a/olpcgames/pangofont.py b/olpcgames/pangofont.py
new file mode 100755
index 0000000..441dfd1
--- /dev/null
+++ b/olpcgames/pangofont.py
@@ -0,0 +1,346 @@
+"""Implement Pygame's font interface using Pango for international support
+
+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.
+"""
+import pango
+import logging
+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 )
+
+# Install myself on top of pygame.font
+def install():
+ """Replace Pygame's font module with this module"""
+ log.info( 'installing' )
+ from olpcgames import pangofont
+ import pygame
+ pygame.font = pangofont
+ import sys
+ sys.modules["pygame.font"] = pangofont
+
+class PangoFont(object):
+ """Base class for a pygame.font.Font-like object drawn by Pango
+
+ Attributes of note:
+
+ fd -- instances Pango FontDescription object
+ WEIGHT_* -- parameters for use with set_weight
+ STYLE_* -- parameters for use with set_style
+
+ """
+ WEIGHT_BOLD = pango.WEIGHT_BOLD
+ WEIGHT_HEAVY = pango.WEIGHT_HEAVY
+ WEIGHT_LIGHT = pango.WEIGHT_LIGHT
+ WEIGHT_NORMAL = pango.WEIGHT_NORMAL
+ WEIGHT_SEMIBOLD = pango.WEIGHT_SEMIBOLD
+ WEIGHT_ULTRABOLD = pango.WEIGHT_ULTRABOLD
+ WEIGHT_ULTRALIGHT = pango.WEIGHT_ULTRALIGHT
+ STYLE_NORMAL = pango.STYLE_NORMAL
+ STYLE_ITALIC = pango.STYLE_ITALIC
+ STYLE_OBLIQUE = pango.STYLE_OBLIQUE
+ def __init__(self, family=None, size=None, bold=False, italic=False, underline=False, fd=None):
+ """If you know what pango.FontDescription (fd) you want, pass it in as
+ 'fd'. Otherwise, specify any number of family, size, bold, or italic,
+ and we will try to match something up for you."""
+
+ # Always set the FontDescription (FIXME - only set it if the user wants
+ # to change something?)
+ if fd is None:
+ fd = pango.FontDescription()
+ 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
+ self.fd = fd
+ self.set_bold( bold )
+ self.set_italic( italic )
+ self.set_underline( underline )
+
+ def render(self, text, antialias=True, color=(255,255,255), background=None ):
+ """Render the font onto a new Surface and return it.
+ We ignore 'antialias' and use system settings.
+
+ text -- (unicode) string with the text to render
+ antialias -- attempt to antialias the text or not
+ color -- three or four-tuple of 0-255 values specifying rendering
+ colour for the text
+ background -- three or four-tuple of 0-255 values specifying rendering
+ colour for the background, or None for trasparent background
+
+ returns a pygame image instance
+ """
+ log.info( 'render: %r, antialias = %s, color=%s, background=%s', text, antialias, color, background )
+
+ layout = self._createLayout( text )
+ # determine pixel size
+ (logical, ink) = layout.get_pixel_extents()
+ ink = pygame.rect.Rect(ink)
+
+ # Create a new Cairo ImageSurface
+ csrf,cctx = _cairoimage.newContext( ink.w, ink.h )
+ cctx = pangocairo.CairoContext(cctx)
+
+ # Mangle the colors on little-endian machines. The reason for this
+ # is that Cairo writes native-endian 32-bit ARGB values whereas
+ # Pygame expects endian-independent values in whatever format. So we
+ # tell our users not to expect transparency here (avoiding the A issue)
+ # and we swizzle all the colors around.
+
+ # render onto it
+ if background is not None:
+ background = _cairoimage.mangle_color( background )
+ cctx.set_source_rgba(*background)
+ cctx.paint()
+
+ log.debug( 'incoming color: %s', color )
+ color = _cairoimage.mangle_color( color )
+ log.debug( ' translated color: %s', color )
+
+ cctx.new_path()
+ cctx.layout_path(layout)
+ cctx.set_source_rgba(*color)
+ cctx.fill()
+
+ # Create and return a new Pygame Image derived from the Cairo Surface
+ return _cairoimage.asImage( csrf )
+
+ def set_bold( self, bold=True):
+ """Set our font description's weight to "bold" or "normal"
+
+ bold -- boolean, whether to set the value to "bold" weight or not
+ """
+ if bold:
+ self.set_weight( self.WEIGHT_BOLD )
+ else:
+ self.set_weight( self.WEIGHT_NORMAL )
+ def set_weight( self, weight ):
+ """Explicitly set our pango-style weight value"""
+ self.fd.set_weight( weight )
+ return self.get_weight()
+ def get_weight( self ):
+ """Explicitly get our pango-style weight value"""
+ return self.fd.get_weight()
+ def get_bold( self ):
+ """Return whether our font's weight is bold (or above)"""
+ return self.fd.get_weight() >= pango.WEIGHT_BOLD
+
+ def set_italic( self, italic=True ):
+ """Set our "italic" value (style)"""
+ if italic:
+ self.set_style( self.STYLE_ITALIC )
+ else:
+ self.set_style( self.STYLE_NORMAL )
+ def set_style( self, style ):
+ """Set our font description's pango-style"""
+ self.fd.set_style( style )
+ return self.fd.get_style()
+ def get_style( self ):
+ """Get our font description's pango-style"""
+ return self.fd.get_style()
+ def get_italic( self ):
+ """Return whether we are currently italicised"""
+ return self.fd.get_style() == self.STYLE_ITALIC # what about oblique?
+
+ def set_underline( self, underline=True ):
+ """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."""
+ def __init__(self, name, size, bold=False, italic=False):
+ fd = pango.FontDescription(name)
+ fd.set_absolute_size(size*pango.SCALE)
+ if bold:
+ fd.set_weight(pango.WEIGHT_BOLD)
+ if italic:
+ fd.set_style(pango.STYLE_OBLIQUE)
+ super(SysFont, self).__init__(fd=fd)
+
+# originally defined a new class, no reason for that...
+NotImplemented = NotImplementedError
+
+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]"
+
+where FAMILY-LIST is a comma separated list of families optionally terminated by a comma, STYLE_OPTIONS is a whitespace separated list of words where each WORD describes one of style, variant, weight, or stretch, and SIZE is an decimal number (size in points). For example the following are all valid string representations:
+
+ "sans bold 12"
+ "serif,monospace bold italic condensed 16"
+ "normal 10"
+
+The commonly available font families are: Normal, Sans, Serif and Monospace. The available styles are:
+Normal the font is upright.
+Oblique the font is slanted, but in a roman style.
+Italic the font is slanted in an italic style.
+
+The available weights are:
+Ultra-Light the ultralight weight (= 200)
+Light the light weight (=300)
+Normal the default weight (= 400)
+Bold the bold weight (= 700)
+Ultra-Bold the ultra-bold weight (= 800)
+Heavy the heavy weight (= 900)
+
+The available variants are:
+Normal
+Small-Caps
+
+The available stretch styles are:
+Ultra-Condensed the smallest width
+Extra-Condensed
+Condensed
+Semi-Condensed
+Normal the normal width
+Semi-Expanded
+Expanded
+Extra-Expanded
+Ultra-Expanded the widest width
+ """
+ fd = pango.FontDescription(name)
+ if bold:
+ fd.set_weight(pango.WEIGHT_BOLD)
+ if italic:
+ fd.set_style(pango.STYLE_OBLIQUE)
+ return PangoFont(fd=fd)
+
+def get_init():
+ """Return boolean indicating whether we are initialised
+
+ Always returns True
+ """
+ return True
+
+def init():
+ """Initialise the module (null operation)"""
+ pass
+
+def quit():
+ """De-initialise the module (null operation)"""
+ pass
+
+def get_default_font():
+ """Return default-font specification to be passed to e.g. fontByDesc"""
+ return "sans"
+
+def get_fonts():
+ """Return the set of all fonts available (currently just 3 generic types)"""
+ return ["sans","serif","monospace"]
+
+
+def stdcolor(color):
+ """Produce a 4-element 0.0-1.0 color value from input"""
+ def fixlen(color):
+ if len(color) == 3:
+ return tuple(color) + (255,)
+ elif len(color) == 4:
+ return color
+ else:
+ raise TypeError("What sort of color is this: %s" % (color,))
+ return [_fixColorBase(x) for x in fixlen(color)]
+def _fixColorBase( v ):
+ """Return a properly clamped colour in floating-point space"""
+ return max((0,min((v,255.0))))/255.0
diff --git a/olpcgames/pausescreen.py b/olpcgames/pausescreen.py
new file mode 100755
index 0000000..113a0ea
--- /dev/null
+++ b/olpcgames/pausescreen.py
@@ -0,0 +1,116 @@
+"""Display a "paused" version of the currently-displayed screen
+
+This code is largely cribbed from the Pippy activity's display code,
+but we try to be a little more generally usable than they are, as
+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
+
+ sleep_timeout -- dormant period before we invoke pause_screen
+ pause -- callable to produce visual notification of pausing, normally
+ 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.
+ to specify a different overlaySVG file)
+
+ returns set of pending events (potentially empty)
+ """
+ if not pause:
+ pause = pauseScreen
+ events = pygame.event.get( )
+ if not events:
+ log.info( 'No events in queue' )
+ old_screen = None
+ if last_event_time() > sleep_timeout:
+ # we've been waiting long enough, go to sleep visually
+ log.warn( 'Pausing activity after %s with function %s', sleep_timeout, pause )
+ old_screen = pause( )
+ if old_screen:
+ pygame.display.flip()
+ # now we wait until there *are* some events (efficiently)
+ # and retrieve any extra events that are waiting...
+ events = [ pygame.event.wait() ] + pygame.event.get()
+ log.warn( 'Activity restarted')
+ if old_screen:
+ restoreScreen( old_screen )
+ if events:
+ _set_last_event_time()
+ return events
+
+def pauseScreen( overlaySVG=None ):
+ """Display a "Paused" screen and suspend
+
+ This default implementation will not do anything to shut down your
+ simulation or other code running in other threads. It will merely block
+ this thread (the pygame thread) until an event shows up in the
+ eventwrap queue.
+
+ Returns a surface to pass to restoreScreen to continue...
+ """
+ from olpcgames import svgsprite
+ if not overlaySVG:
+ from olpcgames.data import sleeping_svg
+ overlaySVG = sleeping_svg.data
+ screen = pygame.display.get_surface()
+ old_screen = screen.copy() # save this for later.
+ pause_sprite = svgsprite.SVGSprite(
+ overlaySVG,
+ )
+ pause_sprite.rect.center = screen.get_rect().center
+ group = sprite.RenderUpdates( )
+ group.add( pause_sprite )
+
+ # dim the screen and display the 'paused' message in the center.
+ BLACK = (0,0,0)
+ WHITE = (255,255,255)
+ dimmed = screen.copy()
+ dimmed.set_alpha(128)
+ screen.fill(BLACK)
+ screen.blit(dimmed, (0,0))
+
+ group.draw( screen )
+ return old_screen
+
+def restoreScreen( old_screen ):
+ """Restore the original screen and return"""
+ screen = pygame.display.get_surface()
+ screen.blit(old_screen, (0,0))
+ return old_screen
diff --git a/olpcgames/svgsprite.py b/olpcgames/svgsprite.py
new file mode 100755
index 0000000..ad247dd
--- /dev/null
+++ b/olpcgames/svgsprite.py
@@ -0,0 +1,84 @@
+"""RSVG/Cairo-based rendering of SVG into Pygame Images"""
+from pygame import sprite, Rect
+from olpcgames import _cairoimage
+
+class SVGSprite( sprite.Sprite ):
+ """Sprite class which renders SVG source-code as a Pygame image
+
+ Note:
+
+ Currently this sprite class is a bit over-engineered, it gets in the way
+ if you want to, e.g. animate among a number of SVG drawings, as it
+ assumes that setSVG will always set a single SVG file for rendering.
+ """
+ rect = image = None
+ resolution = None
+ def __init__(
+ self, svg=None, size=None, *args
+ ):
+ """Initialise the svg sprite
+
+ svg -- svg source text (i.e. content of an svg file)
+ size -- optional, to constrain size, (width,height), leaving one
+ as None or 0 causes proportional scaling, leaving both
+ as None or 0 causes natural scaling (screen resolution)
+ args -- if present, groups to which to automatically add
+ """
+ self.size = size
+ super( SVGSprite, self ).__init__( *args )
+ if svg:
+ self.setSVG( svg )
+ def setSVG( self, svg ):
+ """Set our SVG source"""
+ self.svg = svg
+ # XXX could delay this until actually asked to display...
+ if self.size:
+ width,height = self.size
+ else:
+ width,height = None,None
+ self.image = self._render( width,height ).convert_alpha()
+ rect = self.image.get_rect()
+ if self.rect:
+ rect.move( self.rect ) # should let something higher-level do that...
+ self.rect = rect
+
+ def _render( self, width, height ):
+ """Render our SVG to a Pygame image"""
+ import rsvg
+ handle = rsvg.Handle( data = self.svg )
+ originalSize = (width,height)
+ scale = 1.0
+ hw,hh = handle.get_dimension_data()[:2]
+ if hw and hh:
+ if not width:
+ if not height:
+ width,height = hw,hh
+ else:
+ scale = float(height)/hh
+ width = hh/float(hw) * height
+ elif not height:
+ scale = float(width)/hw
+ height = hw/float(hh) * width
+ else:
+ # scale only, only rendering as large as it is...
+ if width/height > hw/hh:
+ # want it taller than it is...
+ width = hh/float(hw) * height
+ else:
+ height = hw/float(hh) * width
+ scale = float(height)/hh
+
+ csrf, ctx = _cairoimage.newContext( int(width), int(height) )
+ ctx.scale( scale, scale )
+ handle.render_cairo( ctx )
+ return _cairoimage.asImage( csrf )
+ return None
+ def copy( self ):
+ """Create a copy of this sprite without reloading the svg image"""
+ result = self.__class__(
+ size = self.size
+ )
+ result.image = self.image
+ result.rect = Rect(self.rect)
+ result.resolution = self.resolution
+ return result
diff --git a/olpcgames/textsprite.py b/olpcgames/textsprite.py
new file mode 100755
index 0000000..7663630
--- /dev/null
+++ b/olpcgames/textsprite.py
@@ -0,0 +1,40 @@
+"""Simple Sprite sub-class that renders via a PangoFont"""
+from pygame import sprite
+from olpcgames import pangofont
+
+class TextSprite( sprite.Sprite ):
+ """Sprite with a simple text renderer"""
+ image = rect = text = color = background = None
+ def __init__( self, text=None, family=None, size=None, bold=False, italic=False, color=None, background=None ):
+ super( TextSprite, self ).__init__( )
+ self.font = pangofont.PangoFont( family=family, size=size, bold=bold, italic=italic )
+ self.set_color( color )
+ self.set_background( background )
+ self.set_text( text )
+ def set_text( self, text ):
+ """Set our text string and render to a graphic"""
+ self.text = text
+ self.render( )
+ def set_color( self, color =None):
+ """Set our rendering colour (default white)"""
+ self.color = color or (255,255,255)
+ self.render()
+ def set_background( self, color=None ):
+ """Set our background color, default transparent"""
+ self.background = color
+ self.render()
+ def render( self ):
+ """Render our image and rect (or None,None)
+
+ After a render you will need to move the rect member to the
+ correct location on the screen.
+ """
+ if self.text:
+ self.image = self.font.render( self.text, color = self.color, background = self.background )
+ currentRect = self.rect
+ self.rect = self.image.get_rect()
+ if currentRect:
+ self.rect.center = currentRect.center
+ else:
+ self.rect = None
+ self.image = None
diff --git a/olpcgames/util.py b/olpcgames/util.py
new file mode 100755
index 0000000..49a23b0
--- /dev/null
+++ b/olpcgames/util.py
@@ -0,0 +1,79 @@
+"""Abstraction layer for working outside the Sugar environment"""
+import traceback, cStringIO
+import logging
+log = logging.getLogger( 'olpcgames.util' )
+import os
+import os.path
+
+NON_SUGAR_ROOT = '~/.sugar/default/olpcgames'
+
+try:
+ from sugar.activity.activity import get_bundle_path as _get_bundle_path
+ def get_bundle_path( ):
+ """Retrieve bundle path from activity with fix for silly registration bug"""
+ path = _get_bundle_path()
+ if path.endswith( '.activity.activity' ):
+ log.warn( '''Found double .activity suffix in bundle path, truncating: %s''', path )
+ path = path[:-9]
+ return path
+except ImportError:
+ log.warn( '''Do not appear to be running under Sugar, stubbing-in get_bundle_path''' )
+ def get_bundle_path():
+ """Retrieve a substitute data-path for non OLPC systems"""
+ return os.getcwd()
+
+
+def get_activity_root( ):
+ """Return the activity root for data storage operations
+
+ If the activity is present, returns the activity's root,
+ otherwise returns NON_SUGAR_ROOT as the directory.
+ """
+ import olpcgames
+ if olpcgames.ACTIVITY:
+ return olpcgames.ACTIVITY.get_activity_root()
+ else:
+ return os.path.expanduser( NON_SUGAR_ROOT )
+
+def data_path(file_name):
+ """Return the full path to a file in the data sub-directory of the bundle"""
+ return os.path.join(get_bundle_path(), 'data', file_name)
+def tmp_path(file_name):
+ """Return the full path to a file in the temporary directory"""
+ return os.path.join(get_activity_root(), 'tmp', file_name)
+
+def get_traceback(error):
+ """Get formatted traceback from current exception
+
+ error -- Exception instance raised
+
+ Attempts to produce a 10-level traceback as a string
+ that you can log off. Use like so:
+
+ try:
+ doSomething()
+ except Exception, err:
+ log.error(
+ '''Failure during doSomething with X,Y,Z parameters: %s''',
+ util.get_traceback( err ),
+ )
+ """
+ if error is None:
+ error = []
+ for (f,l,func,statement) in traceback.extract_stack()[:-2]:
+ if statement:
+ statement = ': %s'%( statement, )
+ if func:
+ error.append( '%s.%s (%s)%s'%( f,func,l, statement))
+ else:
+ error.append( '%s (%s)%s'%( f,l, statement))
+ return "\n".join( error )
+ else:
+ exception = str(error)
+ file = 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
new file mode 100755
index 0000000..032aa13
--- /dev/null
+++ b/olpcgames/video.py
@@ -0,0 +1,178 @@
+"""Video widget for displaying a gstreamer pipe
+
+Note: currently this module is not all that elegant or useful,
+we need a better recipe for using and working with Video
+under OLPCGames.
+"""
+import logging
+log = logging.getLogger( 'olpcgames.video' )
+#log.setLevel( logging.INFO )
+import os
+import signal
+import pygame
+import weakref
+import olpcgames
+from olpcgames import _gtkmain
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gst
+
+class VideoWidget(gtk.DrawingArea):
+ """Widget to render GStreamer video over our Pygame Canvas
+
+ The VideoWidget is a simple GTK window which is
+ held by the PygameCanvas, just as is the Pygame
+ window we normally use. As such this approach
+ *cannot* work without the GTK wrapper.
+
+ It *should* be possible to use raw X11 operations
+ to create a child window of the Pygame/SDL window
+ and use that for the same purpose, but that would
+ require some pretty low-level ctypes hacking.
+
+ Attributes of Note:
+
+ rect -- Pygame rectangle which tells us where to
+ display ourselves, setting the rect changes the
+ position and size of the window.
+ """
+ _imagesink = None
+ _renderedRect = None
+ def __init__(self, rect=None, force_aspect_ratio=True):
+ super(VideoWidget, self).__init__()
+ self.unset_flags(gtk.DOUBLE_BUFFERED)
+ if rect is None:
+ rect = pygame.Rect( (0,0), (160,120))
+ self.rect = rect
+ self.force_aspect_ratio = force_aspect_ratio
+ self.set_size_request(rect.width,rect.height)
+ olpcgames.WIDGET.put( self, rect.left,rect.top)
+ self._renderedRect = rect
+ self.show()
+
+ def set_rect( self, rect ):
+ """Set our rectangle (area of the screen)"""
+ log.debug( 'Set rectangle: %s', rect )
+ self.set_size_request(rect.width,rect.height)
+ olpcgames.WIDGET.move( self, rect.left,rect.top)
+ self.rect = rect
+
+ def do_expose_event(self, event):
+ """Handle exposure event (trigger redraw by gst)"""
+ if self._imagesink:
+ self._imagesink.expose()
+ return False
+ else:
+ return True
+
+ def set_sink(self, sink):
+ """Set our window-sink for output"""
+ assert self.window.xid
+ self._imagesink = sink
+ self._imagesink.set_xwindow_id(self.window.xid)
+ self._imagesink.set_property('force-aspect-ratio', self.force_aspect_ratio)
+
+class PygameWidget( object ):
+ """Render "full-screen" video to the entire Pygame screen
+
+ Not particularly useful unless this happens to be exactly what you need.
+ """
+ def __init__( self ):
+ try:
+ window_id = pygame.display.get_wm_info()['window']
+ except KeyError, err: # pygame-ctypes...
+ 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 )
+ self._imagesink = sink
+ sink.set_xwindow_id( self.window_id )
+
+#pipe_desc = 'v4l2src ! video/x-raw-yuv,width=160,height=120 ! ffmpegcolorspace ! xvimagesink'
+class Player(object):
+ pipe_desc = 'v4l2src ! ffmpegcolorspace ! video/x-raw-yuv ! xvimagesink'
+ test_pipe_desc = 'videotestsrc ! ffmpegcolorspace ! video/x-raw-yuv ! xvimagesink'
+ _synchronized = False
+ def __init__(self, videowidget, pipe_desc=pipe_desc):
+ self._playing = False
+ self._videowidget = videowidget
+
+ self._pipeline = gst.parse_launch(pipe_desc)
+
+ bus = self._pipeline.get_bus()
+ bus.enable_sync_message_emission()
+ bus.add_signal_watch()
+ bus.connect('sync-message::element', self.on_sync_message)
+ bus.connect('message', self.on_message)
+
+ def play(self):
+ log.info( 'Play' )
+ if self._playing == False:
+ self._pipeline.set_state(gst.STATE_PLAYING)
+ self._playing = True
+
+ def pause(self):
+ log.info( 'Pause' )
+ if self._playing == True:
+ if self._synchronized:
+ log.debug( ' pause already sync\'d' )
+ self._pipeline.set_state(gst.STATE_PAUSED)
+ self._playing = False
+ def stop( self ):
+ """Stop all playback"""
+ self._pipeline.set_state( gst.STATE_NULL )
+
+ def on_sync_message(self, bus, message):
+ log.info( 'Sync: %s', message )
+ if message.structure is None:
+ return
+ if message.structure.get_name() == 'prepare-xwindow-id':
+ self._synchronized = True
+ self._videowidget.set_sink(message.src)
+
+ def on_message(self, bus, message):
+ log.info( 'Message: %s', message )
+ t = message.type
+ if t == gst.MESSAGE_ERROR:
+ err, debug = message.parse_error()
+ log.warn("Video error: (%s) %s" ,err, debug)
+ self._playing = False
+
+if __name__ == "__main__":
+ # Simple testing code...
+ logging.basicConfig()
+ log.setLevel( logging.DEBUG )
+ from pygame import image,display, event
+ import pygame
+ def main():
+ display.init()
+ maxX,maxY = display.list_modes()[0]
+ screen = display.set_mode( (maxX/3, maxY/3 ) )
+
+ display.flip()
+
+ pgw = PygameWidget( )
+ p = Player( pgw, pipe_desc=Player.test_pipe_desc )
+ p.play()
+
+ clock = pygame.time.Clock()
+
+ running = True
+ while running:
+ clock.tick( 60 )
+ for evt in [pygame.event.wait()] + pygame.event.get():
+ if evt.type == pygame.KEYDOWN:
+ if p._playing:
+ p.pause()
+ else:
+ p.play()
+ elif evt.type == pygame.QUIT:
+ p.stop()
+ running = False
+ #display.flip()
+ main()
diff --git a/pgu/LICENSE.txt b/pgu/LICENSE.txt
new file mode 100644
index 0000000..b1e3f5a
--- /dev/null
+++ b/pgu/LICENSE.txt
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/pgu/__init__.py b/pgu/__init__.py
new file mode 100644
index 0000000..2212ede
--- /dev/null
+++ b/pgu/__init__.py
@@ -0,0 +1,7 @@
+"""Phil's pyGame Utilities
+
+
+"""
+__version__ = '0.10.6'
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/algo.py b/pgu/algo.py
new file mode 100644
index 0000000..dedb020
--- /dev/null
+++ b/pgu/algo.py
@@ -0,0 +1,135 @@
+"""Some handy algorithms for use in games, etc.
+
+<p>please note that this file is alpha, and is subject to modification in
+future versions of pgu!</p>
+"""
+print 'pgu.algo','This module is alpha, and is subject to change.'
+
+#def dist(a,b):
+# return abs(a[0]-b[0]) + abs(a[1]-b[1])
+
+class node:
+ def __init__(self,prev,pos,dest):
+ self.prev,self.pos,self.dest = prev,pos,dest
+ if self.prev == None: self.g = 0
+ else: self.g = self.prev.g + 1
+ self.h = dist(pos,dest)
+ self.f = self.g+self.h
+
+
+def astar(start,end,layer,_dist):
+ """uses the a* algorithm to find a path
+
+ <pre>astar(start,end,layer,dist): return [list of positions]</pre>
+
+ <dl>
+ <dt>start<dd>start position
+ <dt>end<dd>end position
+ <dt>layer<dd>a grid where zero cells are open and non-zero cells are walls
+ <dt>dist<dd>a distance function dist(a,b)
+ </dl>
+
+ <p>returns a list of positions from start to end</p>
+ """
+ global dist
+ dist = _dist
+ if layer[start[1]][start[0]]: return [] #start is blocked
+ if layer[end[1]][end[0]]: return [] #end is blocked
+ w,h = len(layer[0]),len(layer)
+ if start[0] < 0 or start[1] < 0 or start[0] >= w or start[1] >= h: return [] #start outside of layer
+ if end[0] < 0 or end[1] < 0 or end[0] >= w or end[1] >= h: return [] #end outside of layer
+
+ opens = []
+ open = {}
+ closed = {}
+ cur = node(None,start,end)
+ open[cur.pos] = cur
+ opens.append(cur)
+ while len(open):
+ cur = opens.pop(0)
+ if cur.pos not in open: continue
+ del open[cur.pos]
+ closed[cur.pos] = cur
+ if cur.pos == end: break
+ for dx,dy in [(0,-1),(1,0),(0,1),(-1,0)]:#(-1,-1),(1,-1),(-1,1),(1,1)]:
+ x,y = pos = cur.pos[0]+dx,cur.pos[1]+dy
+ if layer[y][x]: continue
+ #check for blocks of diagonals
+ if layer[cur.pos[1]+dy][cur.pos[0]]: continue
+ if layer[cur.pos[1]][cur.pos[0]+dx]: continue
+ new = node(cur,pos,end)
+ if pos in open and new.f >= open[pos].f: continue
+ if pos in closed and new.f >= closed[pos].f: continue
+ if pos in open: del open[pos]
+ if pos in closed: del closed[pos]
+ open[pos] = new
+ lo = 0
+ hi = len(opens)
+ while lo < hi:
+ mid = (lo+hi)/2
+ if new.f < opens[mid].f: hi = mid
+ else: lo = mid + 1
+ opens.insert(lo,new)
+
+ if cur.pos != end:
+ return []
+
+ path = []
+ while cur.prev != None:
+ path.append(cur.pos)
+ cur = cur.prev
+ path.reverse()
+ return path
+
+
+def getline(a,b):
+ """returns a path of points from a to b
+
+ <pre>getline(a,b): return [list of points]</pre>
+
+ <dl>
+ <dt>a<dd>starting point
+ <dt>b<dd>ending point
+ </dl>
+
+ <p>returns a list of points from a to b</p>
+ """
+
+ path = []
+
+ x1,y1 = a
+ x2,y2 = b
+ dx,dy = abs(x2-x1),abs(y2-y1)
+
+ if x2 >= x1: xi1,xi2 = 1,1
+ else: xi1,xi2 = -1,-1
+
+ if y2 >= y1: yi1,yi2 = 1,1
+ else: yi1,yi2 = -1,-1
+
+ if dx >= dy:
+ xi1,yi2 = 0,0
+ d = dx
+ n = dx/2
+ a = dy
+ p = dx
+ else:
+ xi2,yi1 = 0,0
+ d = dy
+ n = dy/2
+ a = dx
+ p = dy
+
+ x,y = x1,y1
+ c = 0
+ while c <= p:
+ path.append((x,y))
+ n += a
+ if n > d:
+ n -= d
+ x += xi1
+ y += yi1
+ x += xi2
+ y += yi2
+ c += 1
+ return path
diff --git a/pgu/ani.py b/pgu/ani.py
new file mode 100644
index 0000000..c33d380
--- /dev/null
+++ b/pgu/ani.py
@@ -0,0 +1,90 @@
+"""animation loading and manipulating functions.
+
+<p>please note that this file is alpha, and is subject to modification in
+future versions of pgu!</p>
+"""
+
+print 'pgu.ani','This module is alpha, and is subject to change.'
+
+import math
+import pygame
+
+def _ani_load(tv,name,parts,frames,shape):
+ l = len(frames)
+ #print name,parts,l
+ n = parts.pop()
+ if len(parts):
+ s = l/n
+ for i in xrange(0,n):
+ _ani_load(tv,name + ".%d"%i,parts[:],frames[s*i:s*(i+1)],shape)
+ return
+
+ for i in xrange(0,n):
+ tv.images[name+".%d"%i] = frames[i],shape
+
+def ani_load(tv,name,img,size,shape,parts):
+ """load an animation from an image
+
+ <pre>ani_load(tv,name,image,size,shape,parts)</pre>
+
+ <dl>
+ <dt>tv<dd>vid to load into
+ <dt>name <dd>prefix name to give the images
+ <dt>image <dd>image to load anis from
+ <dt>size <dd>w,h size of image
+ <dt>shape <dd>shape of image (usually a subset of 0,0,w,h) used for collision detection
+ <dt>parts <dd>list of parts to divide the animation into
+ <br>for example parts = [4,5] would yield 4 animations 5 frames long, 20 total
+ <br>for example parts = [a,b,c] would yield ... images['name.a.b.c'] ..., a*b*c total
+ </dl>
+
+ """
+ parts = parts[:]
+ parts.reverse()
+ w,h = size
+ frames = []
+ for y in xrange(0,img.get_height(),h):
+ for x in xrange(0,img.get_width(),w):
+ frames.append(img.subsurface(x,y,w,h))
+ _ani_load(tv,name,parts,frames,shape)
+
+
+def image_rotate(tv,name,img,shape,angles,diff=0):
+ """rotate an image and put it into tv.images
+
+ <pre>image_rotate(tv,name,image,shape,angles,diff=0)</pre>
+
+ <dl>
+ <dt>tv <dd>vid to load into
+ <dt>name <dd>prefix name to give the images
+ <dt>image <dd>image to load anis from
+ <dt>shape <dd>shape fimage (usually a subset of 0,0,w,h) used for collision detection
+ <dt>angles <dd>a list of angles to render in degrees
+ <dt>diff <dd>a number to add to the angles, to correct for source image not actually being at 0 degrees
+ </dl>
+ """
+ w1,h1 = img.get_width(),img.get_height()
+ shape = pygame.Rect(shape)
+ ps = shape.topleft,shape.topright,shape.bottomleft,shape.bottomright
+ for a in angles:
+ img2 = pygame.transform.rotate(img,a+diff)
+ w2,h2 = img2.get_width(),img2.get_height()
+ minx,miny,maxx,maxy = 1024,1024,0,0
+ for x,y in ps:
+ x,y = x-w1/2,y-h1/2
+ a2 = math.radians(a+diff)
+ #NOTE: the + and - are switched from the normal formula because of
+ #the weird way that pygame does the angle...
+ x2 = x*math.cos(a2) + y*math.sin(a2)
+ y2 = y*math.cos(a2) - x*math.sin(a2)
+ x2,y2 = x2+w2/2,y2+h2/2
+ minx = min(minx,x2)
+ miny = min(miny,y2)
+ maxx = max(maxx,x2)
+ maxy = max(maxy,y2)
+ r = pygame.Rect(minx,miny,maxx-minx,maxy-miny)
+ #print r
+ #((ww-w)/2,(hh-h)/2,w,h)
+ tv.images["%s.%d"%(name,a)] = img2,r
+
+
diff --git a/pgu/engine.py b/pgu/engine.py
new file mode 100644
index 0000000..76be583
--- /dev/null
+++ b/pgu/engine.py
@@ -0,0 +1,154 @@
+"""a state engine.
+"""
+import pygame
+from pygame.locals import *
+
+class State:
+ """Template Class -- for a state.
+
+ <pre>State(game,value...)</pre>
+
+ <dl>
+ <dt>game<dd>The state engine.
+ <dt>value<dd>I usually pass in a custom value to a state
+ </dl>
+
+ <p>For all of the template methods, they should return None unless they return
+ a new State to switch the engine to.</p>
+ """
+ def __init__(self,game,value=None):
+ self.game,self.value = game,value
+ def init(self):
+ """Template Method - Initialize the state, called once the first time a state is selected.
+
+ <pre>State.init()</pre>
+ """
+ return
+ def paint(self,screen):
+ """Template Method - Paint the screen. Called once after the state is selected.
+
+ <p>State is responsible for calling <tt>pygame.display.flip()</tt> or whatever.</p>
+
+ <pre>State.paint(screen)</pre>
+ """
+ return
+
+ def repaint(self):
+ """Template Method - Request a repaint of this state.
+
+ <pre>State.repaint()</pre>
+ """
+ self._paint = 1
+ def update(self,screen):
+ """Template Method - Update the screen.
+
+ <p>State is responsible for calling <tt>pygame.display.update(updates)</tt> or whatever.</p>
+
+ <pre>State.update(screen)</pre>
+ """
+ return
+ def loop(self):
+ """Template Method - Run a logic loop, called once per frame.
+
+ <pre>State.loop()</pre>
+ """
+ return
+ def event(self,e):
+ """Template Method - Recieve an event.
+
+ <pre>State.event(e)</pre>
+ """
+ return
+
+class Quit(State):
+ """A state to quit the state engine.
+
+ <pre>Quit(game,value)</pre>
+ """
+
+ def init(self):
+ self.game.quit = 1
+
+class Game:
+ """Template Class - The state engine.
+ """
+ def fnc(self,f,v=None):
+ s = self.state
+ if not hasattr(s,f): return 0
+ f = getattr(s,f)
+ if v != None: r = f(v)
+ else: r = f()
+ if r != None:
+ self.state = r
+ self.state._paint = 1
+ return 1
+ return 0
+
+ def run(self,state,screen=None):
+ """Run the state engine, this is a infinite loop (until a quit occurs).
+
+ <pre>Game.run(state,screen=None)</pre>
+
+ <dl>
+ <dt>game<dd>a state engine
+ <dt>screen<dd>the screen
+ </dl>
+ """
+ self.quit = 0
+ self.state = state
+ if screen != None: self.screen = screen
+
+ self.init()
+
+ while not self.quit:
+ self.loop()
+
+ def loop(self):
+ s = self.state
+ if not hasattr(s,'_init') or s._init:
+ s._init = 0
+ if self.fnc('init'): return
+ else:
+ if self.fnc('loop'): return
+ if not hasattr(s,'_paint') or s._paint:
+ s._paint = 0
+ if self.fnc('paint',self.screen): return
+ else:
+ if self.fnc('update',self.screen): return
+
+ for e in pygame.event.get():
+ #NOTE: this might break API?
+ #if self.event(e): return
+ if not self.event(e):
+ if self.fnc('event',e): return
+
+ self.tick()
+ return
+
+ def init(self):
+ """Template Method - called at the beginning of State.run() to initialize things.
+
+ <pre>Game.init()</pre>
+ """
+ return
+
+ def tick(self):
+ """Template Method - called once per frame, usually for timer purposes.
+
+ <pre>Game.tick()</pre>
+ """
+ pygame.time.wait(10)
+
+ def event(self,e):
+ """Template Method - called with each event, so the engine can capture special events.
+
+ <pre>Game.event(e): return captured</pre>
+
+ <p>return a True value if the event is captured and does not need to be passed onto the current
+ state</p>
+ """
+ if e.type is QUIT:
+ self.state = Quit(self)
+ return 1
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/fonts.py b/pgu/fonts.py
new file mode 100644
index 0000000..ab6f73d
--- /dev/null
+++ b/pgu/fonts.py
@@ -0,0 +1,130 @@
+"""Some handy font-like objects.
+
+<p>please note that this file is alpha, and is subject to modification in
+future versions of pgu!</p>
+"""
+
+print 'pgu.fonts','This module is alpha, and is subject to change.'
+
+import pygame
+from pygame.locals import *
+
+class TileFont:
+ """Creates an instance of the TileFont class. Interface compatible with pygame.Font
+
+ <p>TileFonts are fonts that are stored in a tiled image. Where the image opaque, it assumed that the font is visible. Font color is changed automatically, so it does not work with
+ fonts with stylized coloring.</p>
+
+ <pre>TileFont(fname,size,hints,scale=None,sensitive=False)</pre>
+
+ <dl>
+ <dt>size <dd>the dimensions of the characters
+ <dt>hints <dd>a string of hints "abcdefg..."
+ <dt>scale <dd>size to scale font to
+ <dt>sensitive <dd>case sensitivity
+ </dl>
+ """
+
+ def __init__(self,fname,size,hints,scale=None,sensitive=False):
+
+ self.image = pygame.image.load(fname)
+
+ w,h = self.image.get_width(),self.image.get_height()
+ tw,th = size
+ if not scale: scale = size
+ self._size = size
+ self.scale = scale
+
+ self.chars = {}
+ x,y = 0,0
+ self.sensitive = sensitive
+ if not self.sensitive: hints = hints.lower()
+ for c in hints:
+ if c not in ('\r','\n','\t'):
+ img = self.image.subsurface(x,y,tw,th)
+ self.chars[c] = img
+ x += tw
+ if x >= w: x,y = 0,y+th
+
+ self.colors = {}
+
+ def size(self,text):
+ tw,th = self.scale
+ return len(text)*tw,th
+
+ def render(self,text,antialias=0,color=(255,255,255),background=None):
+ size = self.size(text)
+ scale = self.scale
+ tw,th = self._size
+ if background == None:
+ s = pygame.Surface(size).convert_alpha()
+ s.fill((0,0,0,0))
+ else:
+ s = pygame.Surface(size).convert()
+ s.fill(background)
+
+ if not self.sensitive: text = text.lower()
+
+ if color not in self.colors: self.colors[color] = {}
+ colored = self.colors[color]
+
+ x,y = 0,0
+ for c in text:
+ if c in self.chars:
+ if c not in colored:
+ img = self.chars[c].convert_alpha()
+ for yy in xrange(0,th):
+ for xx in xrange(0,tw):
+ r,g,b,a = img.get_at((xx,yy))
+ if a > 128:
+ img.set_at((xx,yy),color)
+ colored[c] = img
+ img = colored[c]
+ if scale != (tw,th): img = pygame.transform.scale(img,scale)
+ s.blit(img,(x,y))
+ x += scale[0]
+ return s
+
+
+class BorderFont:
+ """a decorator for normal fonts, adds a border. Interface compatible with pygame.Font.
+
+ <pre>BorderFont(font,size=1,color=(0,0,0))</pre>
+
+ <dl>
+ <dt>size <dd>width of border; defaults 0
+ <dt>color <dd>color of border; default (0,0,0)
+ </dl>
+ """
+ def __init__(self,font,size=1,color=(0,0,0)):
+
+ self.font = font
+ self._size = size
+ self.color = color
+
+ def size(self,text):
+ w,h = self.font.size(text)
+ s = self._size
+ return w+s*2,h+s*2
+
+ def render(self,text,antialias=0,color=(255,255,255),background=None):
+ size = self.size(text)
+
+ if background == None:
+ s = pygame.Surface(size).convert_alpha()
+ s.fill((0,0,0,0))
+ else:
+ s = pygame.Surface(size).convert()
+ s.fill(background)
+
+ bg = self.font.render(text,antialias,self.color)
+ fg = self.font.render(text,antialias,color)
+
+ si = self._size
+ dirs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
+ for dx,dy in dirs: s.blit(bg,(si+dx*si,si+dy*si))
+ s.blit(fg,(si,si))
+
+ return s
+
+
diff --git a/pgu/gui/__init__.py b/pgu/gui/__init__.py
new file mode 100644
index 0000000..3bcc967
--- /dev/null
+++ b/pgu/gui/__init__.py
@@ -0,0 +1,32 @@
+import pygame
+from pygame.locals import *
+
+from theme import Theme
+from style import Style
+from widget import Widget
+from surface import subsurface, ProxySurface
+from const import *
+
+from container import Container
+from app import App, Desktop
+from table import Table
+from document import Document
+#html
+from area import SlideBox, ScrollArea, List
+
+from form import Form
+from group import Group
+
+from basic import Spacer, Color, Label, Image
+from button import Icon, Button, Switch, Checkbox, Radio, Tool, Link
+from input import Input, Password
+from keysym import Keysym
+from slider import VSlider, HSlider, VScrollBar, HScrollBar
+from select import Select
+from misc import ProgressBar
+
+from menus import Menus
+from dialog import Dialog, FileDialog
+from textarea import TextArea
+
+from deprecated import Toolbox, action_open, action_setvalue, action_quit, action_exec
diff --git a/pgu/gui/app.py b/pgu/gui/app.py
new file mode 100644
index 0000000..4061636
--- /dev/null
+++ b/pgu/gui/app.py
@@ -0,0 +1,226 @@
+"""
+"""
+import pygame
+from pygame.locals import *
+
+import container
+from const import *
+
+class App(container.Container):
+ """The top-level widget for an application.
+
+ <pre>App(theme=None)</pre>
+
+ <dl>
+ <dt>theme<dd>an instance of a Theme, optional as it will use the default Theme class.
+ </dl>
+
+ <strong>Basic Example</strong>
+ <code>
+ app = gui.App()
+ app.run(widget=widget,screen=screen)
+ </code>
+
+ <strong>Integrated Example</strong>
+ <code>
+ app = gui.App()
+ gui.init(widget=widget)
+ while 1:
+ for e in pygame.event.get():
+ app.event(e)
+ app.update(screen)
+ </code>
+
+
+
+ """
+ def __init__(self,theme=None,**params):
+ App.app = self
+
+ if theme == None:
+ from theme import Theme
+ theme = Theme()
+ self.theme = theme
+
+ params['decorate'] = 'app'
+ container.Container.__init__(self,**params)
+ self._quit = False
+ self.widget = None
+ self._chsize = False
+ self._repaint = False
+
+ self.screen = None
+ self.container = None
+ self.events = []
+
+ def resize(self):
+
+ screen = self.screen
+ w = self.widget
+ wsize = 0
+
+ #5 cases
+
+ #input screen is already set use its size
+ if screen:
+ self.screen = screen
+ width,height = screen.get_width(),screen.get_height()
+
+ #display.screen
+ elif pygame.display.get_surface():
+ screen = pygame.display.get_surface()
+ self.screen = screen
+ width,height = screen.get_width(),screen.get_height()
+
+ #app has width,height
+ elif self.style.width != 0 and self.style.height != 0:
+ screen = pygame.display.set_mode((self.style.width,self.style.height),SWSURFACE)
+ self.screen = screen
+ width,height = screen.get_width(),screen.get_height()
+
+ #widget has width,height, or its own size..
+ else:
+ wsize = 1
+ width,height = w.rect.w,w.rect.h = w.resize()
+ #w._resize()
+ screen = pygame.display.set_mode((width,height),SWSURFACE)
+ self.screen = screen
+
+ #use screen to set up size of this widget
+ self.style.width,self.style.height = width,height
+ self.rect.w,self.rect.h = width,height
+ self.rect.x,self.rect.y = 0,0
+
+ w.rect.x,w.rect.y = 0,0
+ w.rect.w,w.rect.h = w.resize(width,height)
+
+ for w in self.windows:
+ w.rect.w,w.rect.h = w.resize()
+
+ self._chsize = False
+
+
+ def init(self,widget=None,screen=None): #TODO widget= could conflict with module widget
+ """Initialize the application.
+
+ <pre>App.init(widget=None,screen=None)</pre>
+
+ <dl>
+ <dt>widget<dd>main widget
+ <dt>screen<dd>pygame.Surface to render to
+ </dl>
+ """
+
+ App.app = self
+
+ if widget: self.widget = widget
+ if screen: self.screen = screen
+
+ self.resize()
+
+ w = self.widget
+
+ self.widgets = []
+ self.widgets.append(w)
+ w.container = self
+ self.focus(w)
+
+ pygame.key.set_repeat(500,30)
+
+ self._repaint = True
+ self._quit = False
+
+ self.send(INIT)
+
+ def event(self,e):
+ """Pass an event to the main widget.
+
+ <pre>App.event(e)</pre>
+
+ <dl>
+ <dt>e<dd>event
+ </dl>
+ """
+ App.app = self
+ #NOTE: might want to deal with ACTIVEEVENT in the future.
+ self.send(e.type,e)
+ container.Container.event(self,e)
+ if e.type == MOUSEBUTTONUP:
+ if e.button not in (4,5): #ignore mouse wheel
+ sub = pygame.event.Event(CLICK,{
+ 'button':e.button,
+ 'pos':e.pos})
+ self.send(sub.type,sub)
+ container.Container.event(self,sub)
+
+
+ def loop(self):
+ App.app = self
+ s = self.screen
+ for e in pygame.event.get():
+ if not (e.type == QUIT and self.mywindow):
+ self.event(e)
+ us = self.update(s)
+ pygame.display.update(us)
+
+
+ def paint(self,screen):
+ self.screen = screen
+ if self._chsize:
+ self.resize()
+ self._chsize = False
+ if hasattr(self,'background'):
+ self.background.paint(screen)
+ container.Container.paint(self,screen)
+
+ def update(self,screen):
+ """Update the screen.
+
+ <dl>
+ <dt>screen<dd>pygame surface
+ </dl>
+ """
+ self.screen = screen
+ if self._chsize:
+ self.resize()
+ self._chsize = False
+ if self._repaint:
+ self.paint(screen)
+ self._repaint = False
+ return [pygame.Rect(0,0,screen.get_width(),screen.get_height())]
+ else:
+ us = container.Container.update(self,screen)
+ return us
+
+ def run(self,widget=None,screen=None):
+ """Run an application.
+
+ <p>Automatically calls <tt>App.init</tt> and then forever loops <tt>App.event</tt> and <tt>App.update</tt></p>
+
+ <dl>
+ <dt>widget<dd>main widget
+ <dt>screen<dd>pygame.Surface to render to
+ </dl>
+ """
+ self.init(widget,screen)
+ while not self._quit:
+ self.loop()
+ pygame.time.wait(10)
+
+ def reupdate(self,w=None): pass
+ def repaint(self,w=None): self._repaint = True
+ def repaintall(self): self._repaint = True
+ def chsize(self):
+ self._chsize = True
+ self._repaint = True
+
+ def quit(self,value=None): self._quit = True
+
+class Desktop(App):
+ """Create an App using the <tt>desktop</tt> theme class.
+
+ <pre>Desktop()</pre>
+ """
+ def __init__(self,**params):
+ params.setdefault('cls','desktop')
+ App.__init__(self,**params) \ No newline at end of file
diff --git a/pgu/gui/area.py b/pgu/gui/area.py
new file mode 100644
index 0000000..8934cf9
--- /dev/null
+++ b/pgu/gui/area.py
@@ -0,0 +1,435 @@
+"""
+"""
+import os
+
+from const import *
+import surface
+import container, table
+import group
+import basic, button, slider
+
+class SlideBox(container.Container):
+ """A scrollable area with no scrollbars.
+
+ <pre>SlideBox(widget,width,height)</pre>
+
+ <dl>
+ <dt>widget<dd>widget to be able to scroll around
+ <dt>width, height<dd>size of scrollable area
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ c = SlideBox(w,100,100)
+ c.offset = (10,10)
+ c.repaint()
+ </code>
+
+ """
+
+ def __init__(self, widget, width, height, **params):
+ params.setdefault('width', width)
+ params.setdefault('height', height)
+ container.Container.__init__(self, **params)
+ self.offset = [0, 0]
+ self.widget = widget
+
+ def __setattr__(self,k,v):
+ if k == 'widget':
+ if hasattr(self,'widget'):
+ self.remove(self.widget)
+ self.add(v,0,0)
+ self.__dict__[k] = v
+
+
+ def paint(self, s):
+ #if not hasattr(self,'surface'):
+ self.surface = pygame.Surface((self.max_rect.w,self.max_rect.h),0,s)
+ #self.surface.fill((0,0,0,0))
+ import app
+ app.App.app.theme.render(self.surface,self.style.background,pygame.Rect(0,0,self.max_rect.w,self.max_rect.h))
+ self.bkgr = pygame.Surface((s.get_width(),s.get_height()),0,s)
+ self.bkgr.blit(s,(0,0))
+ container.Container.paint(self,self.surface)
+ s.blit(self.surface,(-self.offset[0],-self.offset[1]))
+ self._offset = self.offset[:]
+ return
+
+ def paint_for_when_pygame_supports_other_tricks(self,s):
+ #this would be ideal if pygame had support for it!
+ #and if pgu also had a paint(self,s,rect) method to paint small parts
+ sr = (self.offset[0],self.offset[1],self.max_rect.w,self.max_rect.h)
+ cr = (-self.offset[0],-self.offset[1],s.get_width(),s.get_height())
+ s2 = s.subsurface(sr)
+ s2.set_clip(cr)
+ container.Container.paint(self,s2)
+
+ def proxy_paint(self, s):
+ container.Container.paint(self, surface.ProxySurface(parent=None,
+ rect=self.max_rect,
+ real_surface=s,
+ offset=self.offset))
+ def update(self, s):
+ rects = container.Container.update(self,self.surface)
+
+ rets = []
+ s_rect = pygame.Rect(0,0,s.get_width(),s.get_height())
+
+ if self.offset == self._offset:
+ for r in rects:
+ r2 = r.move((-self.offset[0],-self.offset[1]))
+ if r2.colliderect(s_rect):
+ s.blit(self.surface.subsurface(r),r2)
+ rets.append(r2)
+ else:
+ s.blit(self.bkgr,(0,0))
+ sub = pygame.Rect(self.offset[0],self.offset[1],min(s.get_width(),self.max_rect.w-self.offset[0]),min(s.get_height(),self.max_rect.h-self.offset[1]))
+# print sub
+# print self.surface.get_width(),self.surface.get_height()
+# print s.get_width(),s.get_height()
+# print self.offset
+# print self.style.width,self.style.height
+ s.blit(self.surface.subsurface(sub),(0,0))
+ rets.append(s_rect)
+ self._offset = self.offset[:]
+ return rets
+
+ def proxy_update(self, s):
+ rects = container.Container.update(self, surface.ProxySurface(parent=None,
+ rect=self.max_rect,
+ real_surface=s,
+ offset=self.offset))
+ result = []
+ for r in rects: result.append(pygame.Rect(r).move(self.offset))
+ return result
+
+ def resize(self, width=None, height=None):
+ container.Container.resize(self)
+ self.max_rect = pygame.Rect(self.widget.rect)
+ #self.max_rect.w = max(self.max_rect.w,self.style.width)
+ #self.max_rect.h = max(self.max_rect.h,self.style.height)
+ return self.style.width,self.style.height
+ #self.rect = pygame.Rect(self.rect[0], self.rect[1], self.style.width, self.style.height)
+
+ def event(self, e):
+ if e.type in [MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION]:
+ pos = (e.pos[0] + self.offset[0], e.pos[1] + self.offset[1])
+ if self.max_rect.collidepoint(pos):
+ e_params = {'pos': pos }
+ if e.type == MOUSEMOTION:
+ e_params['buttons'] = e.buttons
+ e_params['rel'] = e.rel
+ else:
+ e_params['button'] = e.button
+ e = pygame.event.Event(e.type, e_params)
+ container.Container.event(self, e)
+
+#class SlideBox(Area):
+# def __init__(self,*args,**params):
+# print 'gui.SlideBox','Scheduled to be renamed to Area.'
+# Area.__init__(self,*args,**params)
+
+class ScrollArea(table.Table):
+ """A scrollable area with scrollbars.
+
+ <pre>ScrollArea(widget,width,height,hscrollbar=True)</pre>
+
+ <dl>
+ <dt>widget<dd>widget to be able to scroll around
+ <dt>width, height<dd>size of scrollable area. Set either to 0 to default to size of widget.
+ <dt>hscrollbar<dd>set to False if you do not wish to have a horizontal scrollbar
+ <dt>vscrollbar<dd>set to False if you do not wish to have a vertical scrollbar
+ <dt>step<dd>set to how far clicks on the icons will step
+ </dl>
+ """
+ def __init__(self, widget, width=0, height=0, hscrollbar=True, vscrollbar=True,step=24, **params):
+ w= widget
+ params.setdefault('cls', 'scrollarea')
+ table.Table.__init__(self, width=width,height=height,**params)
+
+ self.sbox = SlideBox(w, width=width, height=height, cls=self.cls+".content")
+ self.widget = w
+ self.vscrollbar = vscrollbar
+ self.hscrollbar = hscrollbar
+
+ self.step = step
+
+ def __setattr__(self,k,v):
+ if k == 'widget':
+ self.sbox.widget = v
+ self.__dict__[k] = v
+
+ def resize(self,width=None,height=None):
+ widget = self.widget
+ box = self.sbox
+
+ #self.clear()
+ table.Table.clear(self)
+ #print 'resize',self,self._rows
+
+ self.tr()
+ self.td(box)
+
+ widget.rect.w, widget.rect.h = widget.resize()
+ my_width,my_height = self.style.width,self.style.height
+ if not my_width:
+ my_width = widget.rect.w
+ self.hscrollbar = False
+ if not my_height:
+ my_height = widget.rect.h
+ self.vscrollbar = False
+
+ box.style.width,box.style.height = my_width,my_height #self.style.width,self.style.height
+
+ box.rect.w,box.rect.h = box.resize()
+
+ #print widget.rect
+ #print box.rect
+ #r = table.Table.resize(self,width,height)
+ #print r
+ #return r
+
+ #print box.offset
+
+# #this old code automatically adds in a scrollbar if needed
+# #but it doesn't always work
+# self.vscrollbar = None
+# if widget.rect.h > box.rect.h:
+# self.vscrollbar = slider.VScrollBar(box.offset[1],0, 65535, 0,step=self.step)
+# self.td(self.vscrollbar)
+# self.vscrollbar.connect(CHANGE, self._vscrollbar_changed, None)
+#
+# vs = self.vscrollbar
+# vs.rect.w,vs.rect.h = vs.resize()
+# box.style.width = self.style.width - vs.rect.w
+#
+#
+# self.hscrollbar = None
+# if widget.rect.w > box.rect.w:
+# self.hscrollbar = slider.HScrollBar(box.offset[0], 0,65535, 0,step=self.step)
+# self.hscrollbar.connect(CHANGE, self._hscrollbar_changed, None)
+# self.tr()
+# self.td(self.hscrollbar)
+#
+# hs = self.hscrollbar
+# hs.rect.w,hs.rect.h = hs.resize()
+# box.style.height = self.style.height - hs.rect.h
+
+ import app
+ xt,xr,xb,xl = app.App.app.theme.getspacing(box)
+
+
+ if self.vscrollbar:
+ self.vscrollbar = slider.VScrollBar(box.offset[1],0, 65535, 0,step=self.step)
+ self.td(self.vscrollbar)
+ self.vscrollbar.connect(CHANGE, self._vscrollbar_changed, None)
+
+ vs = self.vscrollbar
+ vs.rect.w,vs.rect.h = vs.resize()
+ if self.style.width:
+ box.style.width = self.style.width - (vs.rect.w + xl+xr)
+
+ if self.hscrollbar:
+ self.hscrollbar = slider.HScrollBar(box.offset[0], 0,65535, 0,step=self.step)
+ self.hscrollbar.connect(CHANGE, self._hscrollbar_changed, None)
+ self.tr()
+ self.td(self.hscrollbar)
+
+ hs = self.hscrollbar
+ hs.rect.w,hs.rect.h = hs.resize()
+ if self.style.height:
+ box.style.height = self.style.height - (hs.rect.h + xt + xb)
+
+ if self.hscrollbar:
+ hs = self.hscrollbar
+ hs.min = 0
+ hs.max = widget.rect.w - box.style.width
+ hs.style.width = box.style.width
+ hs.size = hs.style.width * box.style.width / max(1,widget.rect.w)
+ else:
+ box.offset[0] = 0
+
+ if self.vscrollbar:
+ vs = self.vscrollbar
+ vs.min = 0
+ vs.max = widget.rect.h - box.style.height
+ vs.style.height = box.style.height
+ vs.size = vs.style.height * box.style.height / max(1,widget.rect.h)
+ else:
+ box.offset[1] = 0
+
+ #print self.style.width,box.style.width, hs.style.width
+
+ r = table.Table.resize(self,width,height)
+ return r
+
+ def x_resize(self, width=None, height=None):
+ w,h = table.Table.resize(self, width, height)
+ if self.hscrollbar:
+ if self.widget.rect.w <= self.sbox.rect.w:
+ self.hscrollbar.size = self.hscrollbar.style.width
+ else:
+ self.hscrollbar.size = max(20,self.hscrollbar.style.width * self.sbox.rect.w / self.widget.rect.w)
+ self._hscrollbar_changed(None)
+ if self.widget.rect.h <= self.sbox.rect.h:
+ self.vscrollbar.size = self.vscrollbar.style.height
+ else:
+ self.vscrollbar.size = max(20,self.vscrollbar.style.height * self.sbox.rect.h / self.widget.rect.h)
+ self._vscrollbar_changed(None)
+ return w,h
+
+ def _vscrollbar_changed(self, xxx):
+ #y = (self.widget.rect.h - self.sbox.rect.h) * self.vscrollbar.value / 1000
+ #if y >= 0: self.sbox.offset[1] = -y
+ self.sbox.offset[1] = self.vscrollbar.value
+ self.sbox.reupdate()
+
+ def _hscrollbar_changed(self, xxx):
+ #x = (self.widget.rect.w - self.sbox.rect.w) * self.hscrollbar.value / 1000
+ #if x >= 0: self.sbox.offset[0] = -x
+ self.sbox.offset[0] = self.hscrollbar.value
+ self.sbox.reupdate()
+
+
+ def set_vertical_scroll(self, percents):
+ #if not self.vscrollbar: return
+ if not hasattr(self.vscrollbar,'value'): return
+ self.vscrollbar.value = percents #min(max(percents*10, 0), 1000)
+ self._vscrollbar_changed(None)
+
+ def set_horizontal_scroll(self, percents):
+ #if not self.hscrollbar: return
+ if not hasattr(self.hscrollbar,'value'): return
+ self.hscrollbar.value = percents #min(max(percents*10, 0), 1000)
+ self._hscrollbar_changed(None)
+
+
+
+
+class _List_Item(button._button):
+ def __init__(self,label=None,image=None,value=None,**params): #TODO label= could conflict with the module label
+ #param image: an imagez.Image object (optional)
+ #param text: a string object
+ params.setdefault('cls','list.item')
+ button._button.__init__(self,**params)
+ self.group = None
+ self.value = value #(self, value)
+ self.widget = None
+
+ if type(label) == str:
+ label = basic.Label(label, cls=self.cls+".label")
+
+ if image and label:
+ self.widget = container.Container()
+ self.widget.add(image, 0, 0)
+ #HACK: improper use of .resize()
+ image.rect.w,image.rect.h = image.resize()
+ self.widget.add(label, image.rect.w, 0)
+ elif image: self.widget = image
+ elif label: self.widget = label
+
+ self.pcls = ""
+
+ def resize(self,width=None,height=None):
+ self.widget.rect.w,self.widget.rect.h = self.widget.resize()
+ return self.widget.rect.w,self.widget.rect.h
+# self.widget._resize()
+# self.rect.w,self.rect.h = self.widget.rect_margin.w,self.widget.rect_margin.h
+
+ def event(self,e):
+ button._button.event(self,e)
+ if self.group.value == self.value: self.pcls = "down"
+
+ def paint(self,s):
+ if self.group.value == self.value: self.pcls = "down"
+ self.widget.paint(surface.subsurface(s,self.widget.rect))
+
+ def click(self):
+ self.group.value = self.value
+ for w in self.group.widgets:
+ if w != self: w.pcls = ""
+
+
+
+class List(ScrollArea):
+ """A list of items in an area.
+
+ <p>This widget can be a form element, it has a value set to whatever item is selected.</p>
+
+ <pre>List(width,height)</pre>
+ """
+ def _change(self, value):
+ self.value = self.group.value
+ self.send(CHANGE)
+
+ def __init__(self, width, height, **params):
+ params.setdefault('cls', 'list')
+ self.table = table.Table(width=width)
+ ScrollArea.__init__(self, self.table, width, height,hscrollbar=False ,**params)
+
+ self.items = []
+
+ g = group.Group()
+ self.group = g
+ g.connect(CHANGE,self._change,None)
+ self.value = self.group.value = None
+
+ self.add = self._add
+ self.remove = self._remove
+
+ def clear(self):
+ """Clear the list.
+
+ <pre>List.clear()</pre>
+ """
+ self.items = []
+ self.group = group.Group()
+ self.group.connect(CHANGE,self._change,None)
+ self.table.clear()
+ self.set_vertical_scroll(0)
+ self.blur(self.myfocus)
+
+ def _docs(self): #HACK: nasty hack to get the docs in "my way"
+ def add(self, label, image=None, value=None):
+ """Add an item to the list.
+
+ <pre>List.add(label,image=None,value=None)</pre>
+
+ <dl>
+ <dt>label<dd>a label for the item
+ <dt>image<dd>an image for the item
+ <dt>value<dd>a value for the item
+ </dl>
+ """
+
+ def remove(self,value):
+ """Remove an item from the list.
+
+ <pre>List.remove(value)</pre>
+
+ <dl>
+ <dt>value<dd>a value of an item to remove from the list
+ </dl>
+ """
+
+ def _add(self, label, image = None, value=None):
+ item = _List_Item(label,image=image,value=value)
+ self.table.tr()
+ self.table.add(item)
+ self.items.append(item)
+ item.group = self.group
+ item.group.add(item)
+
+ def _remove(self, item):
+ for i in self.items:
+ if i.value == item: item = i
+ if item not in self.items: return
+ item.blur()
+ self.items.remove(item)
+ self.group.widgets.remove(item)
+ self.table.remove_row(item.style.row)
+
+#class List(ListArea):
+# def __init__(self,*args,**params):
+# print 'gui.List','Scheduled to be renamed to ListArea. API may also be changed in the future.'
+# ListArea.__init__(self,*args,**params)
diff --git a/pgu/gui/basic.py b/pgu/gui/basic.py
new file mode 100644
index 0000000..4d9ccff
--- /dev/null
+++ b/pgu/gui/basic.py
@@ -0,0 +1,110 @@
+"""These widgets are all grouped together because they are non-interactive widgets.
+"""
+
+import pygame
+
+from const import *
+import widget
+
+class Spacer(widget.Widget):
+ """A invisible space.
+
+ <pre>Spacer(width,height)</pre>
+
+ """
+ def __init__(self,width,height,**params):
+ params.setdefault('focusable',False)
+ widget.Widget.__init__(self,width=width,height=height,**params)
+
+
+class Color(widget.Widget):
+ """A block of color.
+
+ <p>The color can be changed at run-time.</p>
+
+ <pre>Color(value=None)</pre>
+
+ <strong>Example</strong>
+ <code>
+ c = Color()
+ c.value = (255,0,0)
+ c.value = (0,255,0)
+ </code>
+ """
+
+
+ def __init__(self,value=None,**params):
+ params.setdefault('focusable',False)
+ if value != None: params['value']=value
+ widget.Widget.__init__(self,**params)
+
+ def paint(self,s):
+ if hasattr(self,'value'): s.fill(self.value)
+
+ def __setattr__(self,k,v):
+ if k == 'value' and type(v) == str: v = pygame.Color(v)
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
+
+class Label(widget.Widget):
+ """A text label.
+
+ <pre>Label(value)</pre>
+
+ <dl>
+ <dt>value<dd>text to be displayed
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = Label(value="I own a rubber chicken!")
+
+ w = Label("3 rubber chickens")
+ </code>
+ """
+ def __init__(self,value,**params):
+ params.setdefault('focusable',False)
+ params.setdefault('cls','label')
+ widget.Widget.__init__(self,**params)
+ self.value = value
+ self.font = self.style.font
+ self.style.width, self.style.height = self.font.size(self.value)
+
+ def paint(self,s):
+ s.blit(self.font.render(self.value, 1, self.style.color),(0,0))
+
+class Image(widget.Widget):
+ """An image.
+
+ <pre>Image(value)</pre>
+
+ <dl>
+ <dt>value<dd>a file name or a pygame.Surface
+ </dl>
+
+ """
+ def __init__(self,value,**params):
+ params.setdefault('focusable',False)
+ widget.Widget.__init__(self,**params)
+ if type(value) == str: value = pygame.image.load(value)
+
+ ow,oh = iw,ih = value.get_width(),value.get_height()
+ sw,sh = self.style.width,self.style.height
+
+ if sw and not sh:
+ iw,ih = sw,ih*sw/iw
+ elif sh and not sw:
+ iw,ih = iw*sh/ih,sh
+ elif sw and sh:
+ iw,ih = sw,sh
+
+ if (ow,oh) != (iw,ih):
+ value = pygame.transform.scale(value,(iw,ih))
+ self.style.width,self.style.height = iw,ih
+ self.value = value
+
+ def paint(self,s):
+ s.blit(self.value,(0,0)) \ No newline at end of file
diff --git a/pgu/gui/button.py b/pgu/gui/button.py
new file mode 100644
index 0000000..90cdd1e
--- /dev/null
+++ b/pgu/gui/button.py
@@ -0,0 +1,351 @@
+"""
+"""
+
+from pygame.locals import *
+
+from const import *
+import widget, surface
+import basic
+
+class _button(widget.Widget):
+ def __init__(self,**params):
+ widget.Widget.__init__(self,**params)
+ self.state = 0
+
+ def event(self,e):
+ if e.type == ENTER: self.repaint()
+ elif e.type == EXIT: self.repaint()
+ elif e.type == FOCUS: self.repaint()
+ elif e.type == BLUR: self.repaint()
+ elif e.type == KEYDOWN:
+ if e.key == K_SPACE or e.key == K_RETURN:
+ self.state = 1
+ self.repaint()
+ elif e.type == MOUSEBUTTONDOWN:
+ self.state = 1
+ self.repaint()
+ elif e.type == KEYUP:
+ if self.state == 1:
+ sub = pygame.event.Event(CLICK,{'pos':(0,0),'button':1})
+ #self.send(sub.type,sub)
+ self._event(sub)
+
+ self.state = 0
+ self.repaint()
+ elif e.type == MOUSEBUTTONUP:
+ self.state = 0
+ self.repaint()
+ elif e.type == CLICK:
+ self.click()
+
+ self.pcls = ""
+ if self.state == 0 and self.container.myhover is self:
+ self.pcls = "hover"
+ if self.state == 1 and self.container.myhover is self:
+ self.pcls = "down"
+
+ def click(self):
+ pass
+
+
+class Button(_button):
+ """A button, buttons can be clicked, they are usually used to set up callbacks.
+
+ <pre>Button(value=None)</pre>
+
+ <dl>
+ <dt>value<dd>either a widget or a string
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = gui.Button("Click Me")
+ w.connect(gui.CLICK,fnc,value)
+ </code>
+ """
+ def __init__(self,value=None,**params):
+ params.setdefault('cls','button')
+ _button.__init__(self,**params)
+ self.value = value
+
+ def __setattr__(self,k,v):
+ if k == 'value' and type(v) == str: v = basic.Label(v,cls=self.cls+".label")
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and v != None:
+ pass
+
+ if k == 'value' and _v != NOATTR and _v != None and _v != v:
+ self.send(CHANGE)
+ self.chsize()
+
+ def resize(self,width=None,height=None):
+ self.value.rect.x,self.value.rect.y = 0,0
+ self.value.rect.w,self.value.rect.h = self.value.resize(width,height)
+ return self.value.rect.w,self.value.rect.h
+#
+# self.value._resize()
+# self.rect.w,self.rect.h = self.value.rect_margin.w,self.value.rect_margin.h
+#
+# if self.style.width: self.rect.w = max(self.rect.w,self.style.width)
+# if self.style.height: self.rect.w = max(self.rect.w,self.style.height)
+#
+# xt,xr,xb,xl = self.value.getspacing()
+#
+# self.value._resize(self.rect.w-(xl+xr),self.rect.h-(xt+xb))
+#
+ def paint(self,s):
+ self.value.pcls = self.pcls
+ self.value.paint(surface.subsurface(s,self.value.rect))
+
+class Switch(_button):
+ """A switch can have two states, True or False.
+
+ <pre>Switch(value=False)</pre>
+
+ <dl>
+ <dt>value<dd>initial value, (True, False)
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = gui.Switch(True)
+ w.connect(gui.CHANGE,fnc,value)
+ </code>
+ """
+ def __init__(self,value=False,**params):
+ params.setdefault('cls','switch')
+ _button.__init__(self,**params)
+ self.value = value
+
+ img = self.style.off
+ self.style.width = img.get_width()
+ self.style.height = img.get_height()
+
+ def paint(self,s):
+ #self.pcls = ""
+ #if self.container.myhover is self: self.pcls = "hover"
+ if self.value: img = self.style.on
+ else: img = self.style.off
+ s.blit(img,(0,0))
+
+ def __setattr__(self,k,v):
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
+
+ def click(self):
+ self.value = not self.value
+
+class Checkbox(_button):
+ """Within a Group of Checkbox widgets several may be selected at a time.
+
+ <pre>Checkbox(group,value=None)</pre>
+
+ <dl>
+ <dt>group<dd>a gui.Group for the Checkbox to belong to
+ <dt>value<dd>the value
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ g = gui.Group(name='colors',value=['r','b'])
+
+ t = gui.Table()
+ t.tr()
+ t.td(gui.Label('Red'))
+ t.td(gui.Checkbox(g,'r'))
+ t.tr()
+ t.td(gui.Label('Green'))
+ t.td(gui.Checkbox(g,'g'))
+ t.tr()
+ t.td(gui.Label('Blue'))
+ t.td(gui.Checkbox(g,'b'))
+ </code>
+ """
+
+ def __init__(self,group,value=None,**params):
+ params.setdefault('cls','checkbox')
+ _button.__init__(self,**params)
+ self.group = group
+ self.group.add(self)
+ if self.group.value == None:
+ self.group.value = []
+ self.value = value
+
+ img = self.style.off
+ self.style.width = img.get_width()
+ self.style.height = img.get_height()
+
+ def paint(self,s):
+ #self.pcls = ""
+ #if self.container.myhover is self: self.pcls = "hover"
+ if self.value in self.group.value: img = self.style.on
+ else: img = self.style.off
+
+ s.blit(img,(0,0))
+
+ def click(self):
+ if self.value in self.group.value:
+ self.group.value.remove(self.value)
+ else:
+ self.group.value.append(self.value)
+ self.group._change()
+
+class Radio(_button):
+ """Within a Group of Radio widgets only one may be selected at a time.
+
+ <pre>Radio(group,value=None)</pre>
+
+ <dl>
+ <dt>group<dd>a gui.Group for the Radio to belong to
+ <dt>value<dd>the value
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ g = gui.Group(name='colors',value='g')
+
+ t = gui.Table()
+ t.tr()
+ t.td(gui.Label('Red'))
+ t.td(gui.Radio(g,'r'))
+ t.tr()
+ t.td(gui.Label('Green'))
+ t.td(gui.Radio(g,'g'))
+ t.tr()
+ t.td(gui.Label('Blue'))
+ t.td(gui.Radio(g,'b'))
+ </code>
+ """
+
+
+ def __init__(self,group=None,value=None,**params):
+ params.setdefault('cls','radio')
+ _button.__init__(self,**params)
+ self.group = group
+ self.group.add(self)
+ self.value = value
+
+ img = self.style.off
+ self.style.width = img.get_width()
+ self.style.height = img.get_height()
+
+ def paint(self,s):
+ #self.pcls = ""
+ #if self.container.myhover is self: self.pcls = "hover"
+ if self.group.value == self.value: img = self.style.on
+ else: img = self.style.off
+ s.blit(img,(0,0))
+
+ def click(self):
+ self.group.value = self.value
+
+class Tool(_button):
+ """Within a Group of Tool widgets only one may be selected at a time.
+
+ <pre>Tool(group,widget=None,value=None)</pre>
+
+ <dl>
+ <dt>group<dd>a gui.Group for the Tool to belong to
+ <dt>widget<dd>a widget to appear on the Tool (similar to a Button)
+ <dt>value<dd>the value
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ g = gui.Group(name='colors',value='g')
+
+ t = gui.Table()
+ t.tr()
+ t.td(gui.Tool(g,'Red','r'))
+ t.tr()
+ t.td(gui.Tool(g,'Green','g'))
+ t.tr()
+ t.td(gui.Tool(g,'Blue','b'))
+ </code>
+ """
+
+ def __init__(self,group,widget=None,value=None,**params): #TODO widget= could conflict with module widget
+ params.setdefault('cls','tool')
+ _button.__init__(self,**params)
+ self.group = group
+ self.group.add(self)
+ self.value = value
+
+ if widget:
+ self.setwidget(widget)
+
+ if self.group.value == self.value: self.pcls = "down"
+
+ def setwidget(self,w):
+ self.widget = w
+
+ def resize(self,width=None,height=None):
+ self.widget.rect.w,self.widget.rect.h = self.widget.resize()
+ #self.widget._resize()
+ #self.rect.w,self.rect.h = self.widget.rect_margin.w,self.widget.rect_margin.h
+
+ return self.widget.rect.w,self.widget.rect.h
+
+ def event(self,e):
+ _button.event(self,e)
+ if self.group.value == self.value: self.pcls = "down"
+
+ def paint(self,s):
+ if self.group.value == self.value: self.pcls = "down"
+ self.widget.paint(surface.subsurface(s,self.widget.rect))
+
+ def click(self):
+ self.group.value = self.value
+ for w in self.group.widgets:
+ if w != self: w.pcls = ""
+
+
+class Icon(_button):
+ """TODO - might be deprecated
+ """
+ def __init__(self,cls,**params):
+ params['cls'] = cls
+ _button.__init__(self,**params)
+ s = self.style.image
+ self.style.width = s.get_width()
+ self.style.height = s.get_height()
+ self.state = 0
+
+ def paint(self,s):
+ #self.pcls = ""
+ #if self.state == 0 and hasattr(self.container,'myhover') and self.container.myhover is self: self.pcls = "hover"
+ #if self.state == 1 and hasattr(self.container,'myhover') and self.container.myhover is self: self.pcls = "down"
+ s.blit(self.style.image,(0,0))
+
+class Link(_button):
+ """A link, links can be clicked, they are usually used to set up callbacks.
+ Basically the same as the button widget, just text only with a different cls. Made for
+ convenience.
+
+ <pre>Link(value=None)</pre>
+
+ <dl>
+ <dt>value<dd>a string
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = gui.Link("Click Me")
+ w.connect(gui.CLICK,fnc,value)
+ </code>
+ """
+ def __init__(self,value,**params):
+ params.setdefault('focusable',True)
+ params.setdefault('cls','link')
+ _button.__init__(self,**params)
+ self.value = value
+ self.font = self.style.font
+ self.style.width, self.style.height = self.font.size(self.value)
+
+ def paint(self,s):
+ s.blit(self.font.render(self.value, 1, self.style.color),(0,0))
+
diff --git a/pgu/gui/const.py b/pgu/gui/const.py
new file mode 100644
index 0000000..c865cd8
--- /dev/null
+++ b/pgu/gui/const.py
@@ -0,0 +1,45 @@
+"""Constants.
+<br><br>
+<strong>Event Types</strong>
+
+<p>from pygame</p>
+<dl>
+<dt>QUIT
+<dt>MOUSEBUTTONDOWN
+<dt>MOUSEBUTTONUP
+<dt>MOUSEMOTION
+<dt>KEYDOWN
+</dl>
+
+<p>gui specific</p>
+<dl>
+<dt>ENTER
+<dt>EXIT
+<dt>BLUR
+<dt>FOCUS
+<dt>CLICK
+<dt>CHANGE
+<dt>OPEN
+<dt>CLOSE
+<dt>INIT
+</dl>
+
+<strong>Other</strong>
+<dl>
+<dt>NOATTR
+</dl>
+"""
+import pygame
+
+from pygame.locals import QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION, KEYDOWN, USEREVENT
+ENTER = pygame.locals.USEREVENT + 0
+EXIT = pygame.locals.USEREVENT + 1
+BLUR = pygame.locals.USEREVENT + 2
+FOCUS = pygame.locals.USEREVENT + 3
+CLICK = pygame.locals.USEREVENT + 4
+CHANGE = pygame.locals.USEREVENT + 5
+OPEN = pygame.locals.USEREVENT + 6
+CLOSE = pygame.locals.USEREVENT + 7
+INIT = 'init'
+
+class NOATTR: pass \ No newline at end of file
diff --git a/pgu/gui/container.py b/pgu/gui/container.py
new file mode 100644
index 0000000..739d1a8
--- /dev/null
+++ b/pgu/gui/container.py
@@ -0,0 +1,452 @@
+"""
+"""
+import pygame
+from pygame.locals import *
+
+from const import *
+import widget, surface
+
+class Container(widget.Widget):
+ """The base container widget, can be used as a template as well as stand alone.
+
+ <pre>Container()</pre>
+ """
+ def __init__(self,**params):
+ widget.Widget.__init__(self,**params)
+ self.myfocus = None
+ self.mywindow = None
+ self.myhover = None
+ #self.background = 0
+ self.widgets = []
+ self.windows = []
+ self.toupdate = {}
+ self.topaint = {}
+
+ def update(self,s):
+ updates = []
+
+ if self.myfocus: self.toupdate[self.myfocus] = self.myfocus
+
+ for w in self.topaint:
+ if w is self.mywindow:
+ continue
+ else:
+ sub = surface.subsurface(s,w.rect)
+ sub.blit(w._container_bkgr,(0,0))
+ w.paint(sub)
+ updates.append(pygame.rect.Rect(w.rect))
+
+ for w in self.toupdate:
+ if w is self.mywindow:
+ continue
+ else:
+ us = w.update(surface.subsurface(s,w.rect))
+ if us:
+ for u in us:
+ updates.append(pygame.rect.Rect(u.x + w.rect.x,u.y+w.rect.y,u.w,u.h))
+
+ for w in self.topaint:
+ if w is self.mywindow:
+ w.paint(self.top_surface(s,w))
+ updates.append(pygame.rect.Rect(w.rect))
+ else:
+ continue
+
+ for w in self.toupdate:
+ if w is self.mywindow:
+ us = w.update(self.top_surface(s,w))
+ else:
+ continue
+ if us:
+ for u in us:
+ updates.append(pygame.rect.Rect(u.x + w.rect.x,u.y+w.rect.y,u.w,u.h))
+
+ self.topaint = {}
+ self.toupdate = {}
+
+ return updates
+
+ def repaint(self,w=None):
+ if not w:
+ return widget.Widget.repaint(self)
+ self.topaint[w] = w
+ self.reupdate()
+
+ def reupdate(self,w=None):
+ if not w:
+ return widget.Widget.reupdate(self)
+ self.toupdate[w] = w
+ self.reupdate()
+
+ def paint(self,s):
+ self.toupdate = {}
+ self.topaint = {}
+
+ for w in self.widgets:
+ ok = False
+ try:
+ sub = surface.subsurface(s,w.rect)
+ ok = True
+ except:
+ print 'container.paint(): %s not in %s'%(w.__class__.__name__,self.__class__.__name__)
+ print s.get_width(),s.get_height(),w.rect
+ ok = False
+ if ok:
+ if not (hasattr(w,'_container_bkgr') and w._container_bkgr.get_width() == sub.get_width() and w._container_bkgr.get_height() == sub.get_height()):
+ w._container_bkgr = sub.copy()
+ w._container_bkgr.fill((0,0,0,0))
+ w._container_bkgr.blit(sub,(0,0))
+
+ w.paint(sub)
+
+ for w in self.windows:
+ w.paint(self.top_surface(s,w))
+
+ def top_surface(self,s,w):
+ x,y = s.get_abs_offset()
+ s = s.get_abs_parent()
+ return surface.subsurface(s,(x+w.rect.x,y+w.rect.y,w.rect.w,w.rect.h))
+
+ def event(self,e):
+ used = False
+
+ if self.mywindow and e.type == MOUSEBUTTONDOWN:
+ w = self.mywindow
+ if self.myfocus is w:
+ if not w.rect.collidepoint(e.pos): self.blur(w)
+ if not self.myfocus:
+ if w.rect.collidepoint(e.pos): self.focus(w)
+
+ if not self.mywindow:
+ #### by Gal Koren
+ ##
+ ## if e.type == FOCUS:
+ if e.type == FOCUS and not self.myfocus:
+ #self.first()
+ pass
+ elif e.type == EXIT:
+ if self.myhover: self.exit(self.myhover)
+ elif e.type == BLUR:
+ if self.myfocus: self.blur(self.myfocus)
+ elif e.type == MOUSEBUTTONDOWN:
+ h = None
+ for w in self.widgets:
+ if not w.disabled: #focusable not considered, since that is only for tabs
+ if w.rect.collidepoint(e.pos):
+ h = w
+ if self.myfocus is not w: self.focus(w)
+ if not h and self.myfocus:
+ self.blur(self.myfocus)
+ elif e.type == MOUSEMOTION:
+ if 1 in e.buttons:
+ if self.myfocus: ws = [self.myfocus]
+ else: ws = []
+ else: ws = self.widgets
+
+ h = None
+ for w in ws:
+ if w.rect.collidepoint(e.pos):
+ h = w
+ if self.myhover is not w: self.enter(w)
+ if not h and self.myhover:
+ self.exit(self.myhover)
+ w = self.myhover
+
+ if w and w is not self.myfocus:
+ sub = pygame.event.Event(e.type,{
+ 'buttons':e.buttons,
+ 'pos':(e.pos[0]-w.rect.x,e.pos[1]-w.rect.y),
+ 'rel':e.rel})
+ used = w._event(sub)
+
+ w = self.myfocus
+ if w:
+ sub = e
+
+ if e.type == MOUSEBUTTONUP or e.type == MOUSEBUTTONDOWN:
+ sub = pygame.event.Event(e.type,{
+ 'button':e.button,
+ 'pos':(e.pos[0]-w.rect.x,e.pos[1]-w.rect.y)})
+ used = w._event(sub)
+ elif e.type == CLICK and self.myhover is w:
+ sub = pygame.event.Event(e.type,{
+ 'button':e.button,
+ 'pos':(e.pos[0]-w.rect.x,e.pos[1]-w.rect.y)})
+ used = w._event(sub)
+ elif e.type == CLICK: #a dead click
+ pass
+ elif e.type == MOUSEMOTION:
+ sub = pygame.event.Event(e.type,{
+ 'buttons':e.buttons,
+ 'pos':(e.pos[0]-w.rect.x,e.pos[1]-w.rect.y),
+ 'rel':e.rel})
+ used = w._event(sub)
+ else:
+ used = w._event(sub)
+
+ if not used:
+ if e.type is KEYDOWN:
+ if e.key is K_TAB and self.myfocus:
+ if (e.mod&KMOD_SHIFT) == 0:
+ self.myfocus.next()
+ else:
+ self.myfocus.previous()
+ return True
+ elif e.key == K_UP:
+ self._move_focus(0,-1)
+ return True
+ elif e.key == K_RIGHT:
+ self._move_focus(1,0)
+ return True
+ elif e.key == K_DOWN:
+ self._move_focus(0,1)
+ return True
+ elif e.key == K_LEFT:
+ self._move_focus(-1,0)
+ return True
+ return used
+
+ def _move_focus(self,dx_,dy_):
+ myfocus = self.myfocus
+ if not self.myfocus: return
+
+ from pgu.gui import App
+ widgets = self._get_widgets(App.app)
+ #if myfocus not in widgets: return
+ #widgets.remove(myfocus)
+ if myfocus in widgets:
+ widgets.remove(myfocus)
+ rect = myfocus.get_abs_rect()
+ fx,fy = rect.centerx,rect.centery
+
+ def sign(v):
+ if v < 0: return -1
+ if v > 0: return 1
+ return 0
+
+ dist = []
+ for w in widgets:
+ wrect = w.get_abs_rect()
+ wx,wy = wrect.centerx,wrect.centery
+ dx,dy = wx-fx,wy-fy
+ if dx_ > 0 and wrect.left < rect.right: continue
+ if dx_ < 0 and wrect.right > rect.left: continue
+ if dy_ > 0 and wrect.top < rect.bottom: continue
+ if dy_ < 0 and wrect.bottom > rect.top: continue
+ dist.append((dx*dx+dy*dy,w))
+ if not len(dist): return
+ dist.sort()
+ d,w = dist.pop(0)
+ w.focus()
+
+ def _get_widgets(self,c):
+ widgets = []
+ if c.mywindow:
+ widgets.extend(self._get_widgets(c.mywindow))
+ else:
+ for w in c.widgets:
+ if isinstance(w,Container):
+ widgets.extend(self._get_widgets(w))
+ elif not w.disabled and w.focusable:
+ widgets.append(w)
+ return widgets
+
+ def remove(self,w):
+ """Remove a widget from the container.
+
+ <pre>Container.remove(w)</pre>
+ """
+ self.blur(w)
+ self.widgets.remove(w)
+ #self.repaint()
+ self.chsize()
+
+ def add(self,w,x,y):
+ """Add a widget to the container.
+
+ <pre>Container.add(w,x,y)</pre>
+
+ <dl>
+ <dt>x, y<dd>position of the widget
+ </dl>
+ """
+ w.style.x = x
+ w.style.y = y
+ w.container = self
+ #NOTE: this might fix it, sort of...
+ #but the thing is, we don't really want to resize
+ #something if it is going to get resized again later
+ #for no reason...
+ #w.rect.x,w.rect.y = w.style.x,w.style.y
+ #w.rect.w, w.rect.h = w.resize()
+ self.widgets.append(w)
+ self.chsize()
+
+ def open(self,w=None,x=None,y=None):
+ from app import App #HACK: I import it here to prevent circular importing
+ if not w:
+ if (not hasattr(self,'container') or not self.container) and self is not App.app:
+ self.container = App.app
+ #print 'top level open'
+ return widget.Widget.open(self)
+
+ if self.container:
+ if x != None: return self.container.open(w,self.rect.x+x,self.rect.y+y)
+ return self.container.open(w)
+
+ w.container = self
+
+ if w.rect.w == 0 or w.rect.h == 0: #this might be okay, not sure if needed.
+ #_chsize = App.app._chsize #HACK: we don't want this resize to trigger a chsize.
+ w.rect.w,w.rect.h = w.resize()
+ #App.app._chsize = _chsize
+
+ if x == None or y == None: #auto center the window
+ #w.style.x,w.style.y = 0,0
+ w.rect.x = (self.rect.w-w.rect.w)/2
+ w.rect.y = (self.rect.h-w.rect.h)/2
+ #w.resize()
+ #w._resize(self.rect.w,self.rect.h)
+ else: #show it where we want it
+ w.rect.x = x
+ w.rect.y = y
+ #w._resize()
+
+
+ self.windows.append(w)
+ self.mywindow = w
+ self.focus(w)
+ self.repaint(w)
+ w.send(OPEN)
+
+ def close(self,w=None):
+ if not w:
+ return widget.Widget.close(self)
+
+ if self.container: #make sure we're in the App
+ return self.container.close(w)
+
+ if self.myfocus is w: self.blur(w)
+
+ if w not in self.windows: return #no need to remove it twice! happens.
+
+ self.windows.remove(w)
+
+ self.mywindow = None
+ if self.windows:
+ self.mywindow = self.windows[-1]
+ self.focus(self.mywindow)
+
+ if not self.mywindow:
+ self.myfocus = self.widget #HACK: should be done fancier, i think..
+ if not self.myhover:
+ self.enter(self.widget)
+
+ self.repaintall()
+ w.send(CLOSE)
+
+ def focus(self,w=None):
+ widget.Widget.focus(self) ### by Gal koren
+# if not w:
+# return widget.Widget.focus(self)
+ if not w: return
+ if self.myfocus: self.blur(self.myfocus)
+ if self.myhover is not w: self.enter(w)
+ self.myfocus = w
+ w._event(pygame.event.Event(FOCUS))
+
+ #print self.myfocus,self.myfocus.__class__.__name__
+
+ def blur(self,w=None):
+ if not w:
+ return widget.Widget.blur(self)
+ if self.myfocus is w:
+ if self.myhover is w: self.exit(w)
+ self.myfocus = None
+ w._event(pygame.event.Event(BLUR))
+
+ def enter(self,w):
+ if self.myhover: self.exit(self.myhover)
+ self.myhover = w
+ w._event(pygame.event.Event(ENTER))
+
+ def exit(self,w):
+ if self.myhover and self.myhover is w:
+ self.myhover = None
+ w._event(pygame.event.Event(EXIT))
+
+
+# def first(self):
+# for w in self.widgets:
+# if w.focusable:
+# self.focus(w)
+# return
+# if self.container: self.container.next(self)
+
+# def next(self,w):
+# if w not in self.widgets: return #HACK: maybe. this happens in windows for some reason...
+#
+# for w in self.widgets[self.widgets.index(w)+1:]:
+# if w.focusable:
+# self.focus(w)
+# return
+# if self.container: return self.container.next(self)
+
+
+ def _next(self,orig=None):
+ start = 0
+ if orig in self.widgets: start = self.widgets.index(orig)+1
+ for w in self.widgets[start:]:
+ if not w.disabled and w.focusable:
+ if isinstance(w,Container):
+ if w._next():
+ return True
+ else:
+ self.focus(w)
+ return True
+ return False
+
+ def _previous(self,orig=None):
+ end = len(self.widgets)
+ if orig in self.widgets: end = self.widgets.index(orig)
+ ws = self.widgets[:end]
+ ws.reverse()
+ for w in ws:
+ if not w.disabled and w.focusable:
+ if isinstance(w,Container):
+ if w._previous():
+ return True
+ else:
+ self.focus(w)
+ return True
+ return False
+
+ def next(self,w=None):
+ if w != None and w not in self.widgets: return #HACK: maybe. this happens in windows for some reason...
+
+ if self._next(w): return True
+ if self.container: return self.container.next(self)
+
+
+ def previous(self,w=None):
+ if w != None and w not in self.widgets: return #HACK: maybe. this happens in windows for some reason...
+
+ if self._previous(w): return True
+ if self.container: return self.container.previous(self)
+
+ def resize(self,width=None,height=None):
+ #r = self.rect
+ #r.w,r.h = 0,0
+ ww,hh = 0,0
+ if self.style.width: ww = self.style.width
+ if self.style.height: hh = self.style.height
+
+ for w in self.widgets:
+ #w.rect.w,w.rect.h = 0,0
+ w.rect.x,w.rect.y = w.style.x,w.style.y
+ w.rect.w, w.rect.h = w.resize()
+ #w._resize()
+
+ ww = max(ww,w.rect.right)
+ hh = max(hh,w.rect.bottom)
+ return ww,hh
diff --git a/pgu/gui/deprecated.py b/pgu/gui/deprecated.py
new file mode 100644
index 0000000..9abffc1
--- /dev/null
+++ b/pgu/gui/deprecated.py
@@ -0,0 +1,76 @@
+import pygame
+
+from const import *
+import table
+import group
+import button, basic
+
+def action_open(value):
+ print 'gui.action_open',"Scheduled to be deprecated."
+ value.setdefault('x',None)
+ value.setdefault('y',None)
+ value['container'].open(value['window'],value['x'],value['y'])
+
+def action_setvalue(value):
+ print 'gui.action_setvalue',"Scheduled to be deprecated."
+ a,b = value
+ b.value = a.value
+
+def action_quit(value):
+ print 'gui.action_quit',"Scheduled to be deprecated."
+ value.quit()
+
+def action_exec(value):
+ print 'gui.action_exec',"Scheduled to be deprecated."
+ exec(value['script'],globals(),value['dict'])
+
+class Toolbox(table.Table):
+ def __setattr__(self,k,v):
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.group.value = v
+ for w in self.group.widgets:
+ if w.value != v: w.pcls = ""
+ else: w.pcls = "down"
+ self.repaint()
+
+ def _change(self,value):
+ self.value = self.group.value
+ self.send(CHANGE)
+
+ def __init__(self,data,cols=0,rows=0,tool_cls='tool',value=None,**params):
+ print 'gui.Toolbox','Scheduled to be deprecated.'
+ params.setdefault('cls','toolbox')
+ table.Table.__init__(self,**params)
+
+ if cols == 0 and rows == 0: cols = len(data)
+ if cols != 0 and rows != 0: rows = 0
+
+ self.tools = {}
+
+ _value = value
+
+ g = group.Group()
+ self.group = g
+ g.connect(CHANGE,self._change,None)
+ self.group.value = _value
+
+ x,y,p,s = 0,0,None,1
+ for ico,value in data:
+ #from __init__ import theme
+ import app
+ img = app.App.app.theme.get(tool_cls+"."+ico,"","image")
+ if img:
+ i = basic.Image(img)
+ else: i = basic.Label(ico,cls=tool_cls+".label")
+ p = button.Tool(g,i,value,cls=tool_cls)
+ self.tools[ico] = p
+ #p.style.hexpand = 1
+ #p.style.vexpand = 1
+ self.add(p,x,y)
+ s = 0
+ if cols != 0: x += 1
+ if cols != 0 and x == cols: x,y = 0,y+1
+ if rows != 0: y += 1
+ if rows != 0 and y == rows: x,y = x+1,0
diff --git a/pgu/gui/dialog.py b/pgu/gui/dialog.py
new file mode 100644
index 0000000..d6a89cc
--- /dev/null
+++ b/pgu/gui/dialog.py
@@ -0,0 +1,156 @@
+"""
+"""
+import os
+
+from const import *
+import table, area
+import basic, input, button
+
+class Dialog(table.Table):
+ """A dialog window with a title bar and an "close" button on the bar.
+
+ <pre>Dialog(title,main)</pre>
+
+ <dl>
+ <dt>title<dd>title widget, usually a label
+ <dt>main<dd>main widget, usually a container
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ title = gui.Label("My Title")
+ main = gui.Container()
+ #add stuff to the container...
+
+ d = gui.Dialog(title,main)
+ d.open()
+ </code>
+ """
+ def __init__(self,title,main,**params):
+ params.setdefault('cls','dialog')
+ table.Table.__init__(self,**params)
+
+
+ self.tr()
+ self.td(title,align=-1,cls=self.cls+'.bar')
+ clos = button.Icon(self.cls+".bar.close")
+ clos.connect(CLICK,self.close,None)
+ self.td(clos,align=1,cls=self.cls+'.bar')
+
+ self.tr()
+ self.td(main,colspan=2,cls=self.cls+".main")
+
+
+# self.tr()
+#
+#
+# t = table.Table(cls=self.cls+".bar")
+# t.tr()
+# t.td(title)
+# clos = button.Icon(self.cls+".bar.close")
+# t.td(clos,align=1)
+# clos.connect(CLICK,self.close,None)
+# self.add(t,0,0)
+#
+# main.rect.w,main.rect.h = main.resize()
+# clos.rect.w,clos.rect.h = clos.resize()
+# title.container.style.width = main.rect.w - clos.rect.w
+#
+# self.tr()
+# self.td(main,cls=self.cls+".main")
+#
+
+
+class FileDialog(Dialog):
+ """A file picker dialog window.
+
+ <pre>FileDialog()</pre>
+ <p>Some optional parameters:</p>
+ <dl>
+ <dt>title_txt<dd>title text
+ <dt>button_txt<dd>button text
+ <dt>path<dd>initial path
+ </dl>
+ """
+
+ def __init__(self, title_txt="File Browser", button_txt="Okay", cls="dialog", path=None):
+
+ cls1 = 'filedialog'
+ if not path: self.curdir = os.getcwd()
+ else: self.curdir = path
+ import app
+ self.dir_img = basic.Image(app.App.app.theme.get(cls1+'.folder', '', 'image'))
+ td_style = {'padding_left': 4,
+ 'padding_right': 4,
+ 'padding_top': 2,
+ 'padding_bottom': 2}
+ self.title = basic.Label(title_txt, cls=cls+".title.label")
+ self.body = table.Table()
+ self.list = area.List(width=350, height=150)
+ self.input_dir = input.Input()
+ self.input_file = input.Input()
+ self._list_dir_()
+ self.button_ok = button.Button(button_txt)
+ self.body.tr()
+ self.body.td(basic.Label("Folder"), style=td_style, align=-1)
+ self.body.td(self.input_dir, style=td_style)
+ self.body.tr()
+ self.body.td(self.list, colspan=3, style=td_style)
+ self.list.connect(CHANGE, self._item_select_changed_, None)
+ self.button_ok.connect(CLICK, self._button_okay_clicked_, None)
+ self.body.tr()
+ self.body.td(basic.Label("File"), style=td_style, align=-1)
+ self.body.td(self.input_file, style=td_style)
+ self.body.td(self.button_ok, style=td_style)
+ self.value = None
+ Dialog.__init__(self, self.title, self.body)
+
+ def _list_dir_(self):
+ self.input_dir.value = self.curdir
+ self.input_dir.pos = len(self.curdir)
+ self.input_dir.vpos = 0
+ dirs = []
+ files = []
+ try:
+ for i in os.listdir(self.curdir):
+ if os.path.isdir(os.path.join(self.curdir, i)): dirs.append(i)
+ else: files.append(i)
+ except:
+ self.input_file.value = "Opps! no access"
+ #if '..' not in dirs: dirs.append('..')
+ dirs.sort()
+ dirs = ['..'] + dirs
+
+ files.sort()
+ for i in dirs:
+ #item = ListItem(image=self.dir_img, text=i, value=i)
+ self.list.add(i,image=self.dir_img,value=i)
+ for i in files:
+ #item = ListItem(image=None, text=i, value=i)
+ self.list.add(i,value=i)
+ #self.list.resize()
+ self.list.set_vertical_scroll(0)
+ #self.list.repaintall()
+
+
+ def _item_select_changed_(self, arg):
+ self.input_file.value = self.list.value
+ fname = os.path.abspath(os.path.join(self.curdir, self.input_file.value))
+ if os.path.isdir(fname):
+ self.input_file.value = ""
+ self.curdir = fname
+ self.list.clear()
+ self._list_dir_()
+
+
+ def _button_okay_clicked_(self, arg):
+ if self.input_dir.value != self.curdir:
+ if os.path.isdir(self.input_dir.value):
+ self.input_file.value = ""
+ self.curdir = os.path.abspath(self.input_dir.value)
+ self.list.clear()
+ self._list_dir_()
+ else:
+ self.value = os.path.join(self.curdir, self.input_file.value)
+ self.send(CHANGE)
+ self.close() \ No newline at end of file
diff --git a/pgu/gui/document.py b/pgu/gui/document.py
new file mode 100644
index 0000000..1bd96df
--- /dev/null
+++ b/pgu/gui/document.py
@@ -0,0 +1,112 @@
+"""
+"""
+import pygame
+
+import container
+import layout
+
+class _document_widget:
+ def __init__(self,w,align=None):
+ #w.rect.w,w.rect.h = w.resize()
+ #self.rect = w.rect
+ self.widget = w
+ if align != None: self.align = align
+
+class Document(container.Container):
+ """A document container contains many widgets strung together in a document format. (How informative!)
+
+ <pre>Document()</pre>
+
+ """
+ def __init__(self,**params):
+ params.setdefault('cls','document')
+ container.Container.__init__(self,**params)
+ self.layout = layout.Layout(pygame.Rect(0,0,self.rect.w,self.rect.h))
+
+ def add(self,e,align=None):
+ """Add a widget.
+
+ <pre>Document.add(e,align=None)</pre>
+
+ <dl>
+ <dt>e<dd>widget
+ <dt>align<dd>alignment (None,-1,0,1)
+ </dl>
+ """
+ dw = _document_widget(e,align)
+ self.layout.add(dw)
+ e.container = self
+ e._c_dw = dw
+ self.widgets.append(e)
+ self.chsize()
+
+ def remove(self,e):
+ self.layout._widgets.remove(e._c_dw)
+ self.widgets.remove(e)
+ self.chsize()
+
+
+ def block(self,align):
+ """Start a new block.
+
+ <pre>Document.block(align)</pre>
+
+ <dl>
+ <dt>align<dd>alignment of block (-1,0,1)
+ </dl>
+ """
+ self.layout.add(align)
+
+ def space(self,e):
+ """Add a spacer.
+
+ <pre>Document.space(e)</pre>
+
+ <dl>
+ <dt>e<dd>a (w,h) size for the spacer
+ </dl>
+ """
+ self.layout.add(e)
+
+ def br(self,height):
+ """Add a line break.
+
+ <pre>Document.br(height)</pre>
+
+ <dl>
+ <dt>height<dd>height of line break
+ </dl>
+ """
+ self.layout.add((0,height))
+
+ def resize(self,width=None,height=None):
+ if self.style.width: width = self.style.width
+ if self.style.height: height = self.style.height
+
+ for w in self.widgets:
+ w.rect.w,w.rect.h = w.resize()
+
+ if (width != None and w.rect.w > width) or (height != None and w.rect.h > height):
+ w.rect.w,w.rect.h = w.resize(width,height)
+
+ dw = w._c_dw
+ dw.rect = pygame.Rect(0,0,w.rect.w,w.rect.h)
+
+ if width == None: width = 65535
+ self.layout.rect = pygame.Rect(0,0,width,0)
+ self.layout.resize()
+
+ _max_w = 0
+
+ for w in self.widgets:
+ #xt,xl,xb,xr = w.getspacing()
+ dw = w._c_dw
+ w.style.x,w.style.y,w.rect.w,w.rect.h = dw.rect.x,dw.rect.y,dw.rect.w,dw.rect.h
+ #w.resize()
+ w.rect.x,w.rect.y = w.style.x,w.style.y
+ _max_w = max(_max_w,w.rect.right)
+
+ #self.rect.w = _max_w #self.layout.rect.w
+ #self.rect.h = self.layout.rect.h
+ #print 'document',_max_w,self.layout.rect.h
+ return _max_w,self.layout.rect.h
diff --git a/pgu/gui/form.py b/pgu/gui/form.py
new file mode 100644
index 0000000..9a874f9
--- /dev/null
+++ b/pgu/gui/form.py
@@ -0,0 +1,79 @@
+"""
+"""
+import widget
+
+class Form(widget.Widget):
+ """A form that automatically will contain all named widgets.
+
+ <p>After a form is created, all named widget that are subsequently created are added
+ to that form. You may use dict style access to access named widgets.</p>
+
+ <pre>Form()</pre>
+
+ <strong>Example</strong>
+ <code>
+ f = gui.Form()
+
+ w = gui.Input("Phil",name="firstname")
+ w = gui.Input("Hassey",name="lastname")
+
+ print f.results()
+ print ''
+ print f.items()
+ print ''
+ print f['firstname'].value
+ print f['lastname'].value
+ </code>
+ """
+
+ def __init__(self):
+ widget.Widget.__init__(self,decorate=False)
+ self._elist = []
+ self._emap = {}
+ self._dirty = 0
+ Form.form = self
+
+ def add(self,e,name=None,value=None):
+ if name != None: e.name = name
+ if value != None: e.value = value
+ self._elist.append(e)
+ self._dirty = 1
+
+ def _clean(self):
+ for e in self._elist[:]:
+ if not hasattr(e,'name') or e.name == None:
+ self._elist.remove(e)
+ self._emap = {}
+ for e in self._elist:
+ self._emap[e.name] = e
+ self._dirty = 0
+
+ def __getitem__(self,k):
+ if self._dirty: self._clean()
+ return self._emap[k]
+
+ def __contains__(self,k):
+ if self._dirty: self._clean()
+ if k in self._emap: return True
+ return False
+
+ def results(self):
+ """Return a dict of name => values.
+
+ <pre>Form.results(): return dict</pre>
+ """
+ if self._dirty: self._clean()
+ r = {}
+ for e in self._elist:
+ r[e.name] = e.value
+ return r
+
+ def items(self):
+ """Return a list of name, value keys.
+
+ <pre>Form.items(): return list</pre>
+ """
+ return self.results().items()
+
+ #def start(self):
+ # Object.start(self,-1)
diff --git a/pgu/gui/group.py b/pgu/gui/group.py
new file mode 100644
index 0000000..bcb231a
--- /dev/null
+++ b/pgu/gui/group.py
@@ -0,0 +1,43 @@
+"""
+"""
+from const import *
+import widget
+
+class Group(widget.Widget):
+ """An object for grouping together Form elements.
+
+ <pre>Group(name=None,value=None)</pre>
+
+ <dl>
+ <dt>name<dd>name as used in the Form
+ <dt>value<dd>values that are currently selected in the group
+ </dl>
+
+ <p>See [[gui-button]] for several examples.</p>
+
+ <p>When the value changes, an <tt>gui.CHANGE</tt> event is sent.
+ Although note, that when the value is a list, it may have to be sent by hand via
+ <tt>g.send(gui.CHANGE)</tt></p>
+ """
+
+ def __init__(self,name=None,value=None):
+ widget.Widget.__init__(self,name=name,value=value)
+ self.widgets = []
+
+ def add(self,w):
+ """Add a widget to this group.
+
+ <pre>Group.add(w)</pre>
+ """
+ self.widgets.append(w)
+
+ def __setattr__(self,k,v):
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k] = v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self._change()
+
+ def _change(self):
+ self.send(CHANGE)
+ for w in self.widgets:
+ w.repaint()
diff --git a/pgu/gui/input.py b/pgu/gui/input.py
new file mode 100644
index 0000000..74c3c9e
--- /dev/null
+++ b/pgu/gui/input.py
@@ -0,0 +1,166 @@
+"""
+"""
+import pygame
+from pygame.locals import *
+
+from const import *
+import widget
+
+class Input(widget.Widget):
+ """A single line text input.
+
+ <pre>Input(value="",size=20)</pre>
+
+ <dl>
+ <dt>value<dd>initial text
+ <dt>size<dd>size for the text box, in characters
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = Input(value="Cuzco the Goat",size=20)
+
+ w = Input("Marbles")
+ </code>
+
+ """
+ def __init__(self,value="",size=20,**params):
+ params.setdefault('cls','input')
+ widget.Widget.__init__(self,**params)
+ self.value = value
+ self.pos = len(str(value))
+ self.vpos = 0
+ self.font = self.style.font
+ w,h = self.font.size("e"*size)
+ if not self.style.height: self.style.height = h
+ if not self.style.width: self.style.width = w
+ #self.style.height = max(self.style.height,h)
+ #self.style.width = max(self.style.width,w)
+ #self.rect.w=w+self.style.padding_left+self.style.padding_right;
+ #self.rect.h=h+self.style.padding_top+self.style.padding_bottom;
+
+ def paint(self,s):
+
+ r = pygame.Rect(0,0,self.rect.w,self.rect.h)
+
+ cs = 2 #NOTE: should be in a style
+
+ w,h = self.font.size(self.value[0:self.pos])
+ x = w-self.vpos
+ if x < 0: self.vpos -= -x
+ if x+cs > s.get_width(): self.vpos += x+cs-s.get_width()
+
+ s.blit(self.font.render(self.value, 1, self.style.color),(-self.vpos,0))
+
+ if self.container.myfocus is self:
+ w,h = self.font.size(self.value[0:self.pos])
+ r.x = w-self.vpos
+ r.w = cs
+ r.h = h
+ s.fill(self.style.color,r)
+
+ def _setvalue(self,v):
+ self.__dict__['value'] = v
+ self.send(CHANGE)
+
+ def event(self,e):
+ used = None
+ if e.type == KEYDOWN:
+ if e.key == K_BACKSPACE:
+ if self.pos:
+ self._setvalue(self.value[:self.pos-1] + self.value[self.pos:])
+ self.pos -= 1
+ elif e.key == K_DELETE:
+ if len(self.value) > self.pos:
+ self._setvalue(self.value[:self.pos] + self.value[self.pos+1:])
+ elif e.key == K_HOME:
+ self.pos = 0
+ elif e.key == K_END:
+ self.pos = len(self.value)
+ elif e.key == K_LEFT:
+ if self.pos > 0: self.pos -= 1
+ used = True
+ elif e.key == K_RIGHT:
+ if self.pos < len(self.value): self.pos += 1
+ used = True
+ elif e.key == K_RETURN:
+ self.next()
+ elif e.key == K_TAB:
+ pass
+ else:
+ #c = str(e.unicode)
+ try:
+ c = (e.unicode).encode('latin-1')
+ if c:
+ self._setvalue(self.value[:self.pos] + c + self.value[self.pos:])
+ self.pos += 1
+ except: #ignore weird characters
+ pass
+ self.repaint()
+ elif e.type == FOCUS:
+ self.repaint()
+ elif e.type == BLUR:
+ self.repaint()
+
+ self.pcls = ""
+ if self.container.myfocus is self: self.pcls = "focus"
+
+ return used
+
+ def __setattr__(self,k,v):
+ if k == 'value':
+ if v == None: v = ''
+ v = str(v)
+ self.pos = len(v)
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
+
+class Password(Input):
+ """A password input, text is *-ed out.
+
+ <pre>Password(value="",size=20)</pre>
+
+ <dl>
+ <dt>value<dd>initial text
+ <dt>size<dd>size for the text box, in characters
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = Password(value="password",size=20)
+
+ w = Password("53[r3+")
+ </code>
+
+ """
+
+ def paint(self,s):
+ hidden="*"
+ show=len(self.value)*hidden
+
+ #print "self.value:",self.value
+
+ if self.pos == None: self.pos = len(self.value)
+
+ r = pygame.Rect(0,0,self.rect.w,self.rect.h)
+
+ cs = 2 #NOTE: should be in a style
+
+ w,h = self.font.size(show)
+ x = w-self.vpos
+ if x < 0: self.vpos -= -x
+ if x+cs > s.get_width(): self.vpos += x+cs-s.get_width()
+
+ s.blit(self.font.render(show, 1, self.style.color),(-self.vpos,0))
+
+ if self.container.myfocus is self:
+ #w,h = self.font.size(self.value[0:self.pos])
+ w,h = self.font.size(show[0:self.pos])
+ r.x = w-self.vpos
+ r.w = cs
+ r.h = h
+ s.fill(self.style.color,r)
+
diff --git a/pgu/gui/keysym.py b/pgu/gui/keysym.py
new file mode 100644
index 0000000..cc24089
--- /dev/null
+++ b/pgu/gui/keysym.py
@@ -0,0 +1,72 @@
+"""
+"""
+import pygame
+from pygame.locals import *
+
+from const import *
+import widget
+
+class Keysym(widget.Widget):
+ """A keysym input.
+
+ <p>This widget records the keysym of the key pressed while this widget is in focus.</p>
+
+ <pre>Keysym(value=None)</pre>
+
+ <dl>
+ <dt>value<dd>initial keysym, see <a href="http://www.pygame.org/docs/ref/key.html">pygame keysyms</a> </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = Input(value=pygame.locals.K_g)
+
+ w = Input(pygame.locals.K_g)
+
+ w = Input()
+ </code>
+
+ """
+
+ def __init__(self,value=None,**params):
+ params.setdefault('cls','keysym')
+ widget.Widget.__init__(self,**params)
+ self.value = value
+
+ self.font = self.style.font
+ w,h = self.font.size("Right Super") #"Right Shift")
+ self.style.width,self.style.height = w,h
+ #self.rect.w=w+self.style.padding_left+self.style.padding_right
+ #self.rect.h=h+self.style.padding_top+self.style.padding_bottom
+
+ def event(self,e):
+ used = None
+ if e.type == FOCUS or e.type == BLUR: self.repaint()
+ elif e.type == KEYDOWN:
+ if e.key != K_TAB:
+ self.value = e.key
+ self.repaint()
+ self.send(CHANGE)
+ used = True
+ self.next()
+ self.pcls = ""
+ if self.container.myfocus is self: self.pcls = "focus"
+ return used
+
+ def paint(self,s):
+ r = pygame.rect.Rect(0,0,self.rect.w,self.rect.h)
+ #render_box(s,self.style.background,r)
+ if self.value == None: return
+ name = ""
+ for p in pygame.key.name(self.value).split(): name += p.capitalize()+" "
+ #r.x = self.style.padding_left;
+ #r.y = self.style.padding_bottom;
+ s.blit(self.style.font.render(name, 1, self.style.color), r)
+
+ def __setattr__(self,k,v):
+ if k == 'value' and v != None:
+ v = int(v)
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
diff --git a/pgu/gui/layout.py b/pgu/gui/layout.py
new file mode 100644
index 0000000..01fe0cb
--- /dev/null
+++ b/pgu/gui/layout.py
@@ -0,0 +1,172 @@
+"""document layout engine."""
+class Layout:
+ """the document layout engine
+
+ .widgets -- elements are kept in this list. read-only, use add to add items to it.
+ """
+
+ def __init__(self,rect=None):
+ """initialize the object with the size of the box."""
+ self._widgets = []
+ self.rect = rect
+
+ def add(self,e):
+ """add a document element to the layout.
+
+ a document element may be
+ - a tuple (w,h) if it is a whitespace element
+ - a tuple (0,h) if it is a linebreak element
+ - an integer -1,0,1 if it is a command to start a new block of elements that are aligned either left,center, or right.
+ - an object with a .rect (for size) -- such as a word element
+ - an object with a .rect (for size) and .align -- such as an image element
+ """
+
+ self._widgets.append(e)
+
+
+ def resize(self):
+ """resize the layout
+ this method recalculates the position of all document elements
+ after they have been added to the document. .rect.x,y will be updated for all
+ objects.
+ """
+ self.init()
+ self.widgets = []
+ for e in self._widgets:
+ if type(e) is tuple and e[0] != 0:
+ self.do_space(e)
+ elif type(e) is tuple and e[0] == 0:
+ self.do_br(e[1])
+ elif type(e) is int:
+ self.do_block(align=e)
+ elif hasattr(e,'align'):
+ self.do_align(e)
+ else:
+ self.do_item(e)
+ self.line()
+ self.rect.h = max(self.y,self.left_bottom,self.right_bottom)
+
+ def init(self):
+ self.x,self.y = self.rect.x,self.rect.y
+ self.left = self.rect.left
+ self.right = self.rect.right
+ self.left_bottom = 0
+ self.right_bottom = 0
+ self.y = self.rect.y
+ self.x = self.rect.x
+ self.h = 0
+
+ self.items = []
+ self.align = -1
+
+ def getleft(self):
+ if self.y > self.left_bottom:
+ self.left = self.rect.left
+ return self.left
+
+ def getright(self):
+ if self.y > self.right_bottom:
+ self.right = self.rect.right
+ return self.right
+
+ def do_br(self,h):
+ self.line()
+ self.h = h
+
+ def do_block(self,align=-1):
+ self.line()
+ self.align = align
+
+ def do_align(self,e):
+ align = e.align
+ ox,oy,oh = self.x,self.y,self.h
+ w,h = e.rect.w,e.rect.h
+
+ if align == 0:
+ self.line()
+ self.x = self.rect.left + (self.rect.width-w)/2
+ self.fit = 0
+ elif align == -1:
+ self.line()
+ self.y = max(self.left_bottom,self.y + self.h)
+ self.h = 0
+ self.x = self.rect.left
+ elif align == 1:
+ self.line()
+ self.y = max(self.right_bottom,self.y + self.h)
+ self.h = 0
+ self.x = self.rect.left + (self.rect.width-w)
+
+ e.rect.x,e.rect.y = self.x,self.y
+
+ self.x = self.x + w
+ self.y = self.y
+
+ if align == 0:
+ self.h = max(self.h,h)
+ self.y = self.y + self.h
+ self.x = self.getleft()
+ self.h = 0
+ elif align == -1:
+ self.left = self.x
+ self.left_bottom = self.y + h
+ self.x,self.y,self.h = ox + w,oy,oh
+ elif align == 1:
+ self.right = self.x - w
+ self.right_bottom = self.y + h
+ self.x,self.y,self.h = ox,oy,oh
+
+ self.widgets.append(e)
+
+ def do_space(self,e):
+ w,h = e
+ if self.x+w >= self.getright():
+ self.line()
+ else:
+ self.items.append(e)
+ self.h = max(self.h,h)
+ self.x += w
+
+ def do_item(self,e):
+ w,h = e.rect.w,e.rect.h
+ if self.x+w >= self.getright():
+ self.line()
+ self.items.append(e)
+ self.h = max(self.h,h)
+ self.x += w
+
+ def line(self):
+ x1 = self.getleft()
+ x2 = self.getright()
+ align = self.align
+ y = self.y
+
+ if len(self.items) != 0 and type(self.items[-1]) == tuple:
+ del self.items[-1]
+
+ w = 0
+ for e in self.items:
+ if type(e) == tuple: w += e[0]
+ else: w += e.rect.w
+
+ if align == -1: x = x1
+ elif align == 0:
+ x = x1 + ((x2-x1)-w)/2
+ self.fit = 0
+ elif align == 1: x = x2 - w
+
+ for e in self.items:
+ if type(e) == tuple: x += e[0]
+ else:
+ e.rect.x,e.rect.y = x,y
+ self.widgets.append(e)
+ x += e.rect.w
+
+ self.items = []
+ self.y = self.y + self.h
+ self.x = self.getleft()
+ self.h = 0
+
+
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/gui/menus.py b/pgu/gui/menus.py
new file mode 100644
index 0000000..3f8a08f
--- /dev/null
+++ b/pgu/gui/menus.py
@@ -0,0 +1,121 @@
+"""
+"""
+from const import *
+import table
+import basic, button
+
+class _Menu_Options(table.Table):
+ def __init__(self,menu,**params):
+ table.Table.__init__(self,**params)
+
+ self.menu = menu
+
+ def event(self,e):
+ handled = False
+ arect = self.get_abs_rect()
+
+ if e.type == MOUSEMOTION:
+ abspos = e.pos[0]+arect.x,e.pos[1]+arect.y
+ for w in self.menu.container.widgets:
+ if not w is self.menu:
+ mrect = w.get_abs_rect()
+ if mrect.collidepoint(abspos):
+ self.menu._close(None)
+ w._open(None)
+ handled = True
+
+ if not handled: table.Table.event(self,e)
+
+class _Menu(button.Button):
+ def __init__(self,parent,widget=None,**params): #TODO widget= could conflict with module widget
+ params.setdefault('cls','menu')
+ button.Button.__init__(self,widget,**params)
+
+ self.parent = parent
+
+ self._cls = self.cls
+ self.options = _Menu_Options(self, cls=self.cls+".options")
+
+ self.connect(CLICK,self._open,None)
+
+ self.pos = 0
+
+ def _open(self,value):
+ self.parent.value = self
+ self.pcls = 'down'
+
+ self.repaint()
+ self.container.open(self.options,self.rect.x,self.rect.bottom)
+ self.options.connect(BLUR,self._pass,None)
+ self.options.blur(self.options.myfocus)
+ self.options.connect(BLUR,self._close,None)
+ self.options.focus()
+ self.repaint()
+
+ def _pass(self,value):
+ pass
+
+ def _close(self,value):
+ self.pcls = ''
+ self.parent.value = None
+ self.repaint()
+ self.options.close()
+
+ def _value(self,value):
+ self._close(None)
+ if value['fnc'] != None:
+ value['fnc'](value['value'])
+
+ def event(self,e):
+ button.Button.event(self,e)
+
+ if self.parent.value == self:
+ self.pcls = 'down'
+
+ def add(self,w,fnc=None,value=None):
+ w.style.align = -1
+ b = button.Button(w,cls=self.cls+".option")
+ b.connect(CLICK,self._value,{'fnc':fnc,'value':value})
+
+ self.options.tr()
+ self.options.add(b)
+
+ return b
+
+class Menus(table.Table):
+ """A drop down menu bar.
+
+ <pre>Menus(data)</pre>
+
+ <dl>
+ <dt>data<dd>Menu data, a list of (path,fnc,value), see example below
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ data = [
+ ('File/Save',fnc_save,None),
+ ('File/New',fnc_new,None),
+ ('Edit/Copy',fnc_copy,None),
+ ('Edit/Cut',fnc_cut,None),
+ ('Help/About',fnc_help,help_about_content),
+ ('Help/Reference',fnc_help,help_reference_content),
+ ]
+ w = Menus(data)
+ """
+
+ def __init__(self,data,menu_cls='menu',**params):
+ params.setdefault('cls','menus')
+ table.Table.__init__(self,**params)
+
+ self.value = None
+
+ n,m,mt = 0,None,None
+ for path,cmd,value in data:
+ parts = path.split("/")
+ if parts[0] != mt:
+ mt = parts[0]
+ m = _Menu(self,basic.Label(mt,cls=menu_cls+".label"),cls=menu_cls)
+ self.add(m,n,0)
+ n += 1
+ m.add(basic.Label(parts[1],cls=m.cls+".option.label"),cmd,value)
diff --git a/pgu/gui/misc.py b/pgu/gui/misc.py
new file mode 100644
index 0000000..ba8ab92
--- /dev/null
+++ b/pgu/gui/misc.py
@@ -0,0 +1,43 @@
+from const import *
+import widget
+import app
+
+class ProgressBar(widget.Widget):
+ """A progress bar.
+
+ <pre>ProgressBar(value,min,max)</pre>
+
+ <dl>
+ <dt>value<dd>starting value
+ <dt>min<dd>minimum value rendered on the screen (usually 0)
+ <dt>max<dd>maximum value
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = gui.ProgressBar(0,0,100)
+ w.value = 25
+ </code>
+ """
+
+ def __init__(self,value,min,max,**params):
+ params.setdefault('cls','progressbar')
+ widget.Widget.__init__(self,**params)
+ self.min,self.max,self.value = min,max,value
+
+ def paint(self,s):
+ r = pygame.rect.Rect(0,0,self.rect.w,self.rect.h)
+ r.w = r.w*(self.value-self.min)/(self.max-self.min)
+ self.bar = r
+ app.App.app.theme.render(s,self.style.bar,r)
+
+ def __setattr__(self,k,v):
+ if k == 'value':
+ v = int(v)
+ v = max(v,self.min)
+ v = min(v,self.max)
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint() \ No newline at end of file
diff --git a/pgu/gui/select.py b/pgu/gui/select.py
new file mode 100644
index 0000000..90fcfe4
--- /dev/null
+++ b/pgu/gui/select.py
@@ -0,0 +1,191 @@
+"""
+"""
+from const import *
+import table
+import basic, button
+
+class Select(table.Table):
+ """A select input.
+
+ <pre>Select(value=None)</pre>
+
+ <dl>
+ <dt>value<dd>initial value
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = Select(value="goats")
+ w.add("Cats","cats")
+ w.add("Goats","goats")
+ w.add("Dogs","Dogs")
+
+ w.value = 'dogs' #changes the value from goats to dogs
+ </code>
+
+ """
+
+ def __init__(self,value=None,**params):
+ params.setdefault('cls','select')
+ table.Table.__init__(self,**params)
+
+ self.top_selected = button.Button(cls=self.cls+".selected")
+ table.Table.add(self,self.top_selected) #,hexpand=1,vexpand=1)#,0,0)
+ self.top_selected.value = basic.Label(" ",cls=self.cls+".option.label")
+
+ self.top_arrow = button.Button(basic.Image(self.style.arrow),cls=self.cls+".arrow")
+ table.Table.add(self,self.top_arrow) #,hexpand=1,vexpand=1) #,1,0)
+
+ self.options = table.Table() #style={'border':3})
+ self.options_first = None
+
+ self.options.tr()
+ self.spacer_top = basic.Spacer(0,0)
+ self.options.add(self.spacer_top)
+
+ self.options.tr()
+ self._options = table.Table(cls=self.cls+".options")
+ self.options.add(self._options)
+
+ self.options.tr()
+ self.spacer_bottom = basic.Spacer(0,0)
+ self.options.add(self.spacer_bottom)
+
+
+ self.options.connect(BLUR,self._close,None)
+ self.spacer_top.connect(CLICK,self._close,None)
+ self.spacer_bottom.connect(CLICK,self._close,None)
+
+ self.values = []
+ self.value = value
+
+ def resize(self,width=None,height=None):
+ max_w,max_h = 0,0
+ for w in self._options.widgets:
+ w.rect.w,w.rect.h = w.resize()
+ max_w,max_h = max(max_w,w.rect.w),max(max_h,w.rect.h)
+
+ #xt,xr,xb,xl = self.top_selected.getspacing()
+ self.top_selected.style.width = max_w #+ xl + xr
+ self.top_selected.style.height = max_h #+ xt + xb
+
+ self.top_arrow.connect(CLICK,self._open,None)
+ self.top_selected.connect(CLICK,self._open,None)
+
+ w,h = table.Table.resize(self,width,height)
+
+ self.spacer_top.style.width, self.spacer_top.style.height = w,h
+ self.spacer_bottom.style.width, self.spacer_bottom.style.height = w,h
+ self._options.style.width = w
+ #HACK: sort of, but not a big one..
+ self._options.resize()
+
+ return w,h
+
+ def _open(self,value):
+ sh = self.rect.h #spacer height
+ opts = self.options
+
+ self.spacer_top.style.height = 0
+ self.spacer_bottom.style.height = 0
+ opts.rect.w, opts.rect.h = opts.resize()
+ h = opts.rect.h
+
+ y = self.rect.y
+ c = self.container
+ while hasattr(c,'container'):
+ y += c.rect.y
+ if c.container == None: break
+ c = c.container
+
+ if y + sh + h <= c.rect.h: #down
+ self.spacer_top.style.height = sh
+ dy = self.rect.y
+ else: #up
+ self.spacer_bottom.style.height = sh
+ dy = self.rect.y - h
+
+ opts.rect.w, opts.rect.h = opts.resize()
+
+ self.container.open(opts,self.rect.x,dy)
+ self.options_first.focus()
+
+ def _close(self,value):
+# print 'my close!'
+ self.options.close()
+ self.top_selected.focus()
+# self.blur()
+# self.focus()
+# print self.container.myfocus == self
+
+ def _setvalue(self,value):
+ self.value = value._value
+ if hasattr(self,'container'):
+ #self.chsize()
+ #HACK: improper use of resize()
+ #self.resize() #to recenter the new value, etc.
+ pass
+ # #self._resize()
+
+ self._close(None)
+ #self.repaint() #this will happen anyways
+
+
+
+ def __setattr__(self,k,v):
+ mywidget = None
+ if k == 'value':
+ for w in self.values:
+ if w._value == v:
+ mywidget = w
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
+ if k == 'value':
+ if not mywidget:
+ mywidget = basic.Label(" ",cls=self.cls+".option.label")
+ self.top_selected.value = mywidget
+
+ def add(self,w,value=None):
+ """Add a widget, value item to the Select.
+
+ <pre>Select.add(widget,value=None)</pre>
+
+ <dl>
+ <dt>widget<dd>Widget or string to represent the item
+ <dt>value<dd>value for this item
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = Select()
+ w.add("Goat") #adds a Label
+ w.add("Goat","goat") #adds a Label with the value goat
+ w.add(gui.Label("Cuzco"),"goat") #adds a Label with value goat
+ </code>
+ """
+
+ if type(w) == str: w = basic.Label(w,cls=self.cls+".option.label")
+
+ w.style.align = -1
+ b = button.Button(w,cls=self.cls+".option")
+ b.connect(CLICK,self._setvalue,w)
+ #b = w
+ #w.cls = self.cls+".option"
+ #w.cls = self.cls+".option"
+
+ self._options.tr()
+ self._options.add(b) #,align=-1)
+
+ if self.options_first == None:
+ self.options_first = b
+ #self._options.td(b, align=-1, cls=self.cls+".option")
+ #self._options.td(_List_Item(w,value=value),align=-1)
+
+ if value != None: w._value = value
+ else: w._value = w
+ if self.value == w._value:
+ self.top_selected.value = w
+ self.values.append(w)
diff --git a/pgu/gui/slider.py b/pgu/gui/slider.py
new file mode 100644
index 0000000..3357ae0
--- /dev/null
+++ b/pgu/gui/slider.py
@@ -0,0 +1,281 @@
+"""All sliders and scroll bar widgets have the same parameters.
+
+<pre>Slider(value,min,max,size)</pre>
+<dl>
+<dt>value<dd>initial value
+<dt>min<dd>minimum value
+<dt>max<dd>maximum value
+<dt>size<dd>size of bar in pixels
+</dl>
+"""
+import pygame
+from pygame.locals import *
+
+from const import *
+import widget
+import app
+import table
+import basic
+
+_SLIDER_HORIZONTAL = 0
+_SLIDER_VERTICAL = 1
+
+class _slider(widget.Widget):
+ def __init__(self,value,orient,min,max,size,step=1,**params):
+ params.setdefault('cls','slider')
+ widget.Widget.__init__(self,**params)
+ self.min,self.max,self.value,self.orient,self.size,self.step = min,max,value,orient,size,step
+
+
+ def paint(self,s):
+
+ self.value = self.value
+ r = pygame.rect.Rect(0,0,self.style.width,self.style.height)
+ if self.orient == _SLIDER_HORIZONTAL:
+ r.x = (self.value-self.min) * (r.w-self.size) / max(1,self.max-self.min);
+ r.w = self.size;
+ else:
+ r.y = (self.value-self.min) * (r.h-self.size) / max(1,self.max-self.min);
+ r.h = self.size;
+
+ self.bar = r
+
+ app.App.app.theme.render(s,self.style.bar,r)
+
+ def event(self,e):
+ used = None
+ r = pygame.rect.Rect(0,0,self.style.width,self.style.height)
+ adj = 0
+ if e.type == ENTER: self.repaint()
+ elif e.type == EXIT: self.repaint()
+ elif e.type == MOUSEBUTTONDOWN:
+ if self.bar.collidepoint(e.pos):
+ self.grab = e.pos[0],e.pos[1]
+ self.grab_value = self.value
+ else:
+ x,y,adj = e.pos[0],e.pos[1],1
+ self.grab = None
+ self.repaint()
+ elif e.type == MOUSEBUTTONUP:
+ #x,y,adj = e.pos[0],e.pos[1],1
+ self.repaint()
+ elif e.type == MOUSEMOTION:
+ if 1 in e.buttons and self.container.myfocus is self:
+ if self.grab != None:
+ rel = e.pos[0]-self.grab[0],e.pos[1]-self.grab[1]
+ if self.orient == _SLIDER_HORIZONTAL:
+ d = (r.w - self.size)
+ if d != 0: self.value = self.grab_value + ((self.max-self.min) * rel[0] / d)
+ else:
+ d = (r.h - self.size)
+ if d != 0: self.value = self.grab_value + ((self.max-self.min) * rel[1] / d)
+ else:
+ x,y,adj = e.pos[0],e.pos[1],1
+
+ elif e.type is KEYDOWN:
+ if self.orient == _SLIDER_HORIZONTAL and e.key == K_LEFT:
+ self.value -= self.step
+ used = True
+ elif self.orient == _SLIDER_HORIZONTAL and e.key == K_RIGHT:
+ self.value += self.step
+ used = True
+ elif self.orient == _SLIDER_VERTICAL and e.key == K_UP:
+ self.value -= self.step
+ used = True
+ elif self.orient == _SLIDER_VERTICAL and e.key == K_DOWN:
+ self.value += self.step
+ used = True
+
+ if adj:
+ if self.orient == _SLIDER_HORIZONTAL:
+ d = self.size/2 - (r.w/(self.max-self.min+1))/2
+ self.value = (x-d) * (self.max-self.min) / (r.w-self.size+1) + self.min
+ else:
+ d = self.size/2 - (r.h/(self.max-self.min+1))/2
+ self.value = (y-d) * (self.max-self.min) / (r.h-self.size+1) + self.min
+
+ self.pcls = ""
+ if self.container.myhover is self: self.pcls = "hover"
+ if (self.container.myfocus is self and 1 in pygame.mouse.get_pressed()): self.pcls = "down"
+
+ return used
+
+
+ def __setattr__(self,k,v):
+ if k == 'value':
+ v = int(v)
+ v = max(v,self.min)
+ v = min(v,self.max)
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
+
+ if hasattr(self,'size'):
+ sz = min(self.size,max(self.style.width,self.style.height))
+ sz = max(sz,min(self.style.width,self.style.height))
+ self.__dict__['size'] = sz
+
+ if hasattr(self,'max') and hasattr(self,'min'):
+ if self.max < self.min: self.max = self.min
+
+class VSlider(_slider):
+ """A verticle slider.
+
+ <pre>VSlider(value,min,max,size)</pre>
+ """
+ def __init__(self,value,min,max,size,step=1,**params):
+ params.setdefault('cls','vslider')
+ _slider.__init__(self,value,_SLIDER_VERTICAL,min,max,size,step,**params)
+
+class HSlider(_slider):
+ """A horizontal slider.
+
+ <pre>HSlider(value,min,max,size)</pre>
+ """
+ def __init__(self,value,min,max,size,step=1,**params):
+ params.setdefault('cls','hslider')
+ _slider.__init__(self,value,_SLIDER_HORIZONTAL,min,max,size,step,**params)
+
+class HScrollBar(table.Table):
+ """A horizontal scroll bar.
+
+ <pre>HScrollBar(value,min,max,size,step=1)</pre>
+ """
+ def __init__(self,value,min,max,size,step=1,**params):
+ params.setdefault('cls','hscrollbar')
+
+ table.Table.__init__(self,**params)
+
+ self.slider = _slider(value,_SLIDER_HORIZONTAL,min,max,size,step=step,cls=self.cls+'.slider')
+
+ self.minus = basic.Image(self.style.minus)
+ self.minus.connect(MOUSEBUTTONDOWN,self._click,-1)
+ self.slider.connect(CHANGE,self.send,CHANGE)
+
+ self.minus2 = basic.Image(self.style.minus)
+ self.minus2.connect(MOUSEBUTTONDOWN,self._click,-1)
+
+ self.plus = basic.Image(self.style.plus)
+ self.plus.connect(MOUSEBUTTONDOWN,self._click,1)
+
+ self.size = size
+
+ def _click(self,value):
+ self.slider.value += self.slider.step*value
+
+ def resize(self,width=None,height=None):
+ self.clear()
+ self.tr()
+
+ w = self.style.width
+ h = self.slider.style.height
+ ww = 0
+
+ if w > (h*2 + self.minus.style.width+self.plus.style.width):
+ self.td(self.minus)
+ ww += self.minus.style.width
+
+ self.td(self.slider)
+
+ if w > (h*2 + self.minus.style.width+self.minus2.style.width+self.plus.style.width):
+ self.td(self.minus2)
+ ww += self.minus2.style.width
+
+ if w > (h*2 + self.minus.style.width+self.plus.style.width):
+ self.td(self.plus)
+ ww += self.plus.style.width
+
+
+ #HACK: handle theme sizing properly
+ from app import App
+ xt,xr,xb,xl = App.app.theme.getspacing(self.slider)
+ ww += xr+xl
+
+ self.slider.style.width = self.style.width - ww
+ setattr(self.slider,'size',self.size * self.slider.style.width / max(1,self.style.width))
+ return table.Table.resize(self,width,height)
+
+
+ def __setattr__(self,k,v):
+ if k in ('min','max','value','step'):
+ return setattr(self.slider,k,v)
+ self.__dict__[k]=v
+
+ def __getattr__(self,k):
+ if k in ('min','max','value','step'):
+ return getattr(self.slider,k)
+ return table.Table.__getattr__(self,k) #self.__dict__[k]
+
+class VScrollBar(table.Table):
+ """A vertical scroll bar.
+
+ <pre>VScrollBar(value,min,max,size,step=1)</pre>
+ """
+ def __init__(self,value,min,max,size,step=1,**params):
+ params.setdefault('cls','vscrollbar')
+
+ table.Table.__init__(self,**params)
+
+ self.minus = basic.Image(self.style.minus)
+ self.minus.connect(MOUSEBUTTONDOWN,self._click,-1)
+
+ self.minus2 = basic.Image(self.style.minus)
+ self.minus2.connect(MOUSEBUTTONDOWN,self._click,-1)
+
+ self.plus = basic.Image(self.style.plus)
+ self.plus.connect(MOUSEBUTTONDOWN,self._click,1)
+
+ self.slider = _slider(value,_SLIDER_VERTICAL,min,max,size,step=step,cls=self.cls+'.slider')
+ self.slider.connect(CHANGE,self.send,CHANGE)
+
+ self.size = size
+
+ def _click(self,value):
+ self.slider.value += self.slider.step*value
+
+ def resize(self,width=None,height=None):
+ self.clear()
+
+ h = self.style.height
+ w = self.slider.style.width
+ hh = 0
+
+ if h > (w*2 + self.minus.style.height+self.plus.style.height):
+ self.tr()
+ self.td(self.minus)
+ hh += self.minus.style.height
+
+ self.tr()
+ self.td(self.slider)
+
+ if h > (w*2 + self.minus.style.height+self.minus2.style.height+self.plus.style.height):
+ self.tr()
+ self.td(self.minus2)
+ hh += self.minus2.style.height
+
+ if h > (w*2 + self.minus.style.height+self.plus.style.height):
+ self.tr()
+ self.td(self.plus)
+ hh += self.plus.style.height
+
+
+ #HACK: handle theme sizing properly
+ from app import App
+ xt,xr,xb,xl = App.app.theme.getspacing(self.slider)
+ hh += xt+xb
+
+ self.slider.style.height = self.style.height - hh
+ setattr(self.slider,'size',self.size * self.slider.style.height / max(1,self.style.height))
+ return table.Table.resize(self,width,height)
+
+ def __setattr__(self,k,v):
+ if k in ('min','max','value','step'):
+ return setattr(self.slider,k,v)
+ self.__dict__[k]=v
+
+ def __getattr__(self,k):
+ if k in ('min','max','value','step'):
+ return getattr(self.slider,k)
+ return table.Table.__getattr__(self,k)
diff --git a/pgu/gui/style.py b/pgu/gui/style.py
new file mode 100644
index 0000000..e7d0c43
--- /dev/null
+++ b/pgu/gui/style.py
@@ -0,0 +1,42 @@
+"""
+"""
+
+class Style:
+ """The class used by widget for the widget.style
+
+ <p>This object is used mainly as a dictionary, accessed via <tt>widget.style.attr</tt>, as opposed to
+ <tt>widget.style['attr']</tt>. It automatically grabs information from the theme via <tt>value = theme.get(widget.cls,widget.pcls,attr)</tt>.</p>
+
+ """
+ def __init__(self,o,dict):
+ self.obj = o
+ for k,v in dict.items(): self.__dict__[k]=v
+ self._cache = {}
+
+ def __getattr__(self,k):
+ key = self.obj.cls,self.obj.pcls,k
+ if key not in self._cache:
+ #import app
+ #self._cache[key] = app.App.app.theme.get(self.obj.cls, self.obj.pcls, k)
+ self._cache[key] = Style_get(self.obj.cls,self.obj.pcls,k)
+ v = self._cache[key]
+ if k in (
+ 'border_top','border_right','border_bottom','border_left',
+ 'padding_top','padding_right','padding_bottom','padding_left',
+ 'margin_top','margin_right','margin_bottom','margin_left',
+ 'align','valign','width','height',
+ ): self.__dict__[k] = v
+ return v
+
+ def __setattr__(self,k,v):
+ self.__dict__[k] = v
+
+
+Style_cache = {}
+def Style_get(cls,pcls,k):
+ key = cls,pcls,k
+ if key not in Style_cache:
+ import app
+ Style_cache[key] = app.App.app.theme.get(cls,pcls,k)
+ return Style_cache[key]
+
diff --git a/pgu/gui/surface.py b/pgu/gui/surface.py
new file mode 100644
index 0000000..9a67928
--- /dev/null
+++ b/pgu/gui/surface.py
@@ -0,0 +1,135 @@
+"""
+"""
+import pygame
+
+def subsurface(s,r):
+ """Return the subsurface of a surface, with some help, checks.
+
+ <pre>subsurface(s,r): return surface</pre>
+ """
+ r = pygame.Rect(r)
+ if r.x < 0 or r.y < 0:
+ raise "gui.subsurface: %d %d %s"%(s.get_width(),s.get_height(),r)
+ w,h = s.get_width(),s.get_height()
+ if r.right > w:
+ r.w -= r.right-w
+ if r.bottom > h:
+ r.h -= r.bottom-h
+ return s.subsurface(r)
+
+class ProxySurface:
+ """
+ A surface-like object which smartly handle out-of-area blitting.
+
+ <pre>ProxySurface(parent, rect, real_surface=None, offset=(0, 0))</pre>
+
+ <p>only one of parent and real_surface should be supplied (non None)</p>
+ <dl>
+ <dt>parent<dd>a ProxySurface object
+ <dt>real_surface<dd>a pygame Surface object
+ </dl>
+
+ <strong>Variables</strong>
+
+ <dl>
+ <dt>mysubsurface<dd>a real and valid pygame.Surface object to be used
+ for blitting.
+ <dt>x, y<dd>if the proxy surface is lefter or higher than the parent,
+ x, y hold the diffs.
+ <dt>offset<dd>an optional feature which let you scroll the whole blitted
+ content.
+ </dl>
+ """
+ def __init__(self, parent, rect, real_surface, offset=(0, 0)):
+ self.offset = offset
+ self.x = self.y = 0
+ if rect[0] < 0: self.x = rect[0]
+ if rect[1] < 0: self.y = rect[1]
+ self.real_surface = real_surface
+ if real_surface == None:
+ self.mysubsurface = parent.mysubsurface.subsurface(parent.mysubsurface.get_rect().clip(rect))
+ else:
+ self.mysubsurface = real_surface.subsurface(real_surface.get_rect().clip(rect))
+ rect[0], rect[1] = 0, 0
+ self.rect = rect
+
+ def blit(self, s, pos, rect=None):
+ if rect == None: rect = s.get_rect()
+ pos = (pos[0] + self.offset[0] + self.x, pos[1] + self.offset[1] + self.y)
+ self.mysubsurface.blit(s, pos, rect)
+
+ def subsurface(self, rect): return ProxySurface(self, pygame.Rect(rect).move(self.offset[0] + self.x, self.offset[1] + self.y),self.real_surface)
+ def fill(self, color, rect=None):
+ if rect != None: self.mysubsurface.fill(color, rect)
+ else: self.mysubsurface.fill(color)
+ def get_rect(self): return self.rect
+ def get_width(self): return self.rect[2]
+ def get_height(self): return self.rect[3]
+ def get_abs_offset(): return self.rect[:2]
+ def get_abs_parent(): return self.mysubsurface.get_abs_parent()
+ def set_clip(self, rect=None):
+ if rect == None: self.mysubsurface.set_clip()
+ else:
+ rect = [rect[0] + self.offset[0] + self.x, rect[1] + self.offset[0] + self.y, rect[2], rect[3]]
+ self.mysubsurface.set_clip(rect)
+
+
+
+
+
+
+class xProxySurface:
+ """
+ A surface-like object which smartly handle out-of-area blitting.
+
+ <pre>ProxySurface(parent, rect, real_surface=None, offset=(0, 0))</pre>
+
+ <p>only one of parent and real_surface should be supplied (non None)</p>
+ <dl>
+ <dt>parent<dd>a ProxySurface object
+ <dt>real_surface<dd>a pygame Surface object
+ </dl>
+
+ <strong>Variables</strong>
+
+ <dl>
+ <dt>mysubsurface<dd>a real and valid pygame.Surface object to be used
+ for blitting.
+ <dt>x, y<dd>if the proxy surface is lefter or higher than the parent,
+ x, y hold the diffs.
+ <dt>offset<dd>an optional feature which let you scroll the whole blitted
+ content.
+ </dl>
+ """
+ def __init__(self, parent, rect, real_surface, offset=(0, 0)):
+ self.offset = offset
+ self.x = self.y = 0
+ if rect[0] < 0: self.x = rect[0]
+ if rect[1] < 0: self.y = rect[1]
+ self.real_surface = real_surface
+ if real_surface == None:
+ self.mysubsurface = parent.mysubsurface.subsurface(parent.mysubsurface.get_rect().clip(rect))
+ else:
+ self.mysubsurface = real_surface.subsurface(real_surface.get_rect().clip(rect))
+ rect[0], rect[1] = 0, 0
+ self.rect = rect
+
+ def blit(self, s, pos, rect=None):
+ if rect == None: rect = s.get_rect()
+ pos = (pos[0] + self.offset[0] + self.x, pos[1] + self.offset[1] + self.y)
+ self.mysubsurface.blit(s, pos, rect)
+
+ def subsurface(self, rect): return ProxySurface(self, pygame.Rect(rect).move(self.offset[0] + self.x, self.offset[1] + self.y),self.real_surface)
+ def fill(self, color, rect=None):
+ if rect != None: self.mysubsurface.fill(color, rect)
+ else: self.mysubsurface.fill(color)
+ def get_rect(self): return self.rect
+ def get_width(self): return self.rect[2]
+ def get_height(self): return self.rect[3]
+ def get_abs_offset(): return self.rect[:2]
+ def get_abs_parent(): return self.mysubsurface.get_abs_parent()
+ def set_clip(self, rect=None):
+ if rect == None: self.mysubsurface.set_clip()
+ else:
+ rect = [rect[0] + self.offset[0] + self.x, rect[1] + self.offset[0] + self.y, rect[2], rect[3]]
+ self.mysubsurface.set_clip(rect)
diff --git a/pgu/gui/table.py b/pgu/gui/table.py
new file mode 100644
index 0000000..6ff6c74
--- /dev/null
+++ b/pgu/gui/table.py
@@ -0,0 +1,331 @@
+"""
+"""
+from const import *
+import container
+
+class Table(container.Container):
+ """A table style container.
+
+ <p>If you know HTML, this should all work roughly how you would expect. If you are not
+ familiar with HTML, please read <a href="http://www.w3.org/TR/REC-html40/struct/tables.html">Tables in HTML Documents</a>. Pay attention to TABLE, TR, TD related parts of the document.</p>
+
+ <pre>Table()</pre>
+
+ <strong>Example</strong>
+ <code>
+ t = gui.Table()
+
+ t.tr()
+ t.td(gui.Label("First Name"), align=-1)
+ t.td(gui.Input())
+
+ t.tr()
+ t.td(gui.Label("Last Name"), align=-1)
+ t.td(gui.Input())
+ </code>
+
+ """
+
+
+ def __init__(self, **params):
+ params.setdefault('cls','table')
+ container.Container.__init__(self, **params)
+ self._rows = []
+ self._curRow = 0
+ self._trok = False
+
+ def getRows(self):
+ return len(self._rows)
+
+ def getColumns(self):
+ if self._rows:
+ return len(self._rows[0])
+ else:
+ return 0
+
+ def remove_row(self, n): #NOTE: won't work in all cases.
+ if n >= self.getRows():
+ print "Trying to remove a nonexistant row:", n, "there are only", self.getRows(), "rows"
+ return
+
+ for cell in self._rows[n]:
+ if isinstance(cell, dict) and cell["widget"] in self.widgets:
+ #print 'removing widget'
+ self.widgets.remove(cell["widget"])
+ del self._rows[n]
+ #print "got here"
+
+ for w in self.widgets:
+ if w.style.row > n: w.style.row -= 1
+
+ if self._curRow >= n:
+ self._curRow -= 1
+
+ #self.rect.w, self.rect.h = self.resize()
+ #self.repaint()
+
+ self.chsize()
+
+ def clear(self):
+ self._rows = []
+ self._curRow = 0
+ self._trok = False
+
+ self.widgets = []
+
+ self.chsize()
+
+ #print 'clear',self,self._rows
+
+ def _addRow(self):
+ self._rows.append([None for x in xrange(self.getColumns())])
+
+ def tr(self):
+ """Start on the next row."""
+ if not self._trok:
+ self._trok = True
+ return
+ self._curRow += 1
+ if self.getRows() <= self._curRow:
+ self._addRow()
+
+ def _addColumn(self):
+ if not self._rows:
+ self._addRow()
+ for row in self._rows:
+ row.append(None)
+
+ def _setCell(self, w, col, row, colspan=1, rowspan=1):
+ #make room for the widget by adding columns and rows
+ while self.getColumns() < col + colspan:
+ self._addColumn()
+ while self.getRows() < row + rowspan:
+ self._addRow()
+
+ #print w.__class__.__name__,col,row,colspan,rowspan
+
+ #actual widget setting and modification stuff
+ w.container = self
+ w.style.row = row #HACK - to work with gal's list
+ w.style.col = col #HACK - to work with gal's list
+ self._rows[row][col] = {"widget":w, "colspan":colspan, "rowspan":rowspan}
+ self.widgets.append(self._rows[row][col]["widget"])
+
+ #set the spanned columns
+ #for acell in xrange(col + 1, col + colspan):
+ # self._rows[row][acell] = True
+
+ #set the spanned rows and the columns on them
+ #for arow in xrange(row + 1, row + rowspan):
+ # for acell in xrange(col, col + colspan): #incorrect?
+ # self._rows[arow][acell] = True
+
+ for arow in xrange(row, row + rowspan):
+ for acell in xrange(col, col + colspan): #incorrect?
+ if row != arow or col != acell:
+ self._rows[arow][acell] = True
+
+
+ def td(self, w, col=None, row=None, colspan=1, rowspan=1, **params):
+ """Add a widget to a table after wrapping it in a TD container.
+
+ <pre>Table.td(w,col=None,row=None,colspan=1,rowspan=1,**params)</pre>
+
+ <dl>
+ <dt>w<dd>widget
+ <dt>col<dd>column
+ <dt>row<dd>row
+ <dt>colspan<dd>colspan
+ <dt>rowspan<dd>rowspan
+ <dt>align<dd>horizontal alignment (-1,0,1)
+ <dt>valign<dd>vertical alignment (-1,0,1)
+ <dt>params<dd>other params for the TD container, style information, etc
+ </dl>
+ """
+
+ Table.add(self,_Table_td(w, **params), col=col, row=row, colspan=colspan, rowspan=rowspan)
+
+ def add(self, w, col=None, row=None, colspan=1, rowspan=1):
+ """Add a widget directly into the table, without wrapping it in a TD container.
+
+ <pre>Table.add(w,col=None,row=None,colspan=1,rowspan=1)</pre>
+
+ <p>See Table.td for an explanation of the parameters.</p>
+ """
+ self._trok = True
+ #if no row was specifically specified, set it to the current row
+ if row is None:
+ row = self._curRow
+ #print row
+
+ #if its going to be a new row, have it be on the first column
+ if row >= self.getRows():
+ col = 0
+
+ #try to find an open cell for the widget
+ if col is None:
+ for cell in xrange(self.getColumns()):
+ if col is None and not self._rows[row][cell]:
+ col = cell
+ break
+
+ #otherwise put the widget in a new column
+ if col is None:
+ col = self.getColumns()
+
+ self._setCell(w, col, row, colspan=colspan, rowspan=rowspan)
+
+ self.chsize()
+ return
+
+ def remove(self,w):
+ if hasattr(w,'_table_td'): w = w._table_td
+ row,col = w.style.row,w.style.col
+ cell = self._rows[row][col]
+ colspan,rowspan = cell['colspan'],cell['rowspan']
+
+ for arow in xrange(row , row + rowspan):
+ for acell in xrange(col, col + colspan): #incorrect?
+ self._rows[arow][acell] = False
+ self.widgets.remove(w)
+ self.chsize()
+
+
+
+ def resize(self, width=None, height=None):
+ #if 1 or self.getRows() == 82:
+ #print ''
+ #print 'resize',self.getRows(),self.getColumns(),width,height
+ #import inspect
+ #for obj,fname,line,fnc,code,n in inspect.stack()[9:20]:
+ # print fname,line,':',fnc,code[0].strip()
+
+
+ #resize the widgets to their smallest size
+ for w in self.widgets:
+ w.rect.w, w.rect.h = w.resize()
+
+ #calculate row heights and column widths
+ rowsizes = [0 for y in xrange(self.getRows())]
+ columnsizes = [0 for x in xrange(self.getColumns())]
+ for row in xrange(self.getRows()):
+ for cell in xrange(self.getColumns()):
+ if self._rows[row][cell] and self._rows[row][cell] is not True:
+ if not self._rows[row][cell]["colspan"] > 1:
+ columnsizes[cell] = max(columnsizes[cell], self._rows[row][cell]["widget"].rect.w)
+ if not self._rows[row][cell]["rowspan"] > 1:
+ rowsizes[row] = max(rowsizes[row], self._rows[row][cell]["widget"].rect.h)
+
+ #distribute extra space if necessary for wide colspanning/rowspanning
+ for row in xrange(self.getRows()):
+ for cell in xrange(self.getColumns()):
+ if self._rows[row][cell] and self._rows[row][cell] is not True:
+ if self._rows[row][cell]["colspan"] > 1:
+ columns = xrange(cell, cell + self._rows[row][cell]["colspan"])
+ totalwidth = 0
+ for acol in columns:
+ totalwidth += columnsizes[acol]
+ if totalwidth < self._rows[row][cell]["widget"].rect.w:
+ for acol in columns:
+ columnsizes[acol] += _table_div(self._rows[row][cell]["widget"].rect.w - totalwidth, self._rows[row][cell]["colspan"],acol)
+ if self._rows[row][cell]["rowspan"] > 1:
+ rows = xrange(row, row + self._rows[row][cell]["rowspan"])
+ totalheight = 0
+ for arow in rows:
+ totalheight += rowsizes[arow]
+ if totalheight < self._rows[row][cell]["widget"].rect.h:
+ for arow in rows:
+ rowsizes[arow] += _table_div(self._rows[row][cell]["widget"].rect.h - totalheight, self._rows[row][cell]["rowspan"],arow)
+
+ #make everything fill out to self.style.width, self.style.heigh, not exact, but pretty close...
+ w, h = sum(columnsizes), sum(rowsizes)
+ if w > 0 and w < self.style.width and len(columnsizes):
+ d = (self.style.width - w)
+ for n in xrange(0, len(columnsizes)):
+ v = columnsizes[n]
+ columnsizes[n] += v * d / w
+ if h > 0 and h < self.style.height and len(rowsizes):
+ d = (self.style.height - h) / len(rowsizes)
+ for n in xrange(0, len(rowsizes)):
+ v = rowsizes[n]
+ rowsizes[n] += v * d / h
+
+ #set the widget's position by calculating their row/column x/y offset
+ cellpositions = [[[sum(columnsizes[0:cell]), sum(rowsizes[0:row])] for cell in xrange(self.getColumns())] for row in xrange(self.getRows())]
+ for row in xrange(self.getRows()):
+ for cell in xrange(self.getColumns()):
+ if self._rows[row][cell] and self._rows[row][cell] is not True:
+ x, y = cellpositions[row][cell]
+ w = sum(columnsizes[cell:cell+self._rows[row][cell]["colspan"]])
+ h = sum(rowsizes[row:row+self._rows[row][cell]["rowspan"]])
+
+ widget = self._rows[row][cell]["widget"]
+ widget.rect.x = x
+ widget.rect.y = y
+ if 1 and (w,h) != (widget.rect.w,widget.rect.h):
+# if h > 20:
+# print widget.widget.__class__.__name__, (widget.rect.w,widget.rect.h),'=>',(w,h)
+ widget.rect.w, widget.rect.h = widget.resize(w, h)
+
+ #print self._rows[row][cell]["widget"].rect
+
+ #print columnsizes
+ #print sum(columnsizes)
+ #size = sum(columnsizes), sum(rowsizes); print size
+
+ #return the tables final size
+ return sum(columnsizes),sum(rowsizes)
+
+
+def _table_div(a,b,c):
+ v,r = a/b, a%b
+ if r != 0 and (c%b)<r: v += 1
+ return v
+
+class _Table_td(container.Container):
+ def __init__(self,widget,**params):#hexpand=0,vexpand=0,
+ container.Container.__init__(self,**params)
+ self.widget = widget
+ #self.hexpand=hexpand
+ #self.vexpand=vexpand
+ widget._table_td = self
+ self.add(widget,0,0)
+
+ def resize(self,width=None,height=None):
+ w = self.widget
+
+ #expansion code, but i didn't like the idea that much..
+ #a bit obscure, fairly useless when a user can just
+ #add a widget to a table instead of td it in.
+ #ww,hh=None,None
+ #if self.hexpand: ww = self.style.width
+ #if self.vexpand: hh = self.style.height
+ #if self.hexpand and width != None: ww = max(ww,width)
+ #if self.vexpand and height != None: hh = max(hh,height)
+ #w.rect.w,w.rect.h = w.resize(ww,hh)
+
+ #why bother, just do the lower mentioned item...
+ w.rect.w,w.rect.h = w.resize()
+
+ #this should not be needed, widgets should obey their sizing on their own.
+
+# if (self.style.width!=0 and w.rect.w > self.style.width) or (self.style.height!=0 and w.rect.h > self.style.height):
+# ww,hh = None,None
+# if self.style.width: ww = self.style.width
+# if self.style.height: hh = self.style.height
+# w.rect.w,w.rect.h = w.resize(ww,hh)
+
+
+ #in the case that the widget is too big, we try to resize it
+ if (width != None and width < w.rect.w) or (height != None and height < w.rect.h):
+ w.rect.w,w.rect.h = w.resize(width,height)
+
+ width = max(width,w.rect.w,self.style.width) #,self.style.cell_width)
+ height = max(height,w.rect.h,self.style.height) #,self.style.cell_height)
+
+ dx = width-w.rect.w
+ dy = height-w.rect.h
+ w.rect.x = (self.style.align+1)*dx/2
+ w.rect.y = (self.style.valign+1)*dy/2
+
+ return width,height
diff --git a/pgu/gui/textarea.py b/pgu/gui/textarea.py
new file mode 100644
index 0000000..6a7c9e6
--- /dev/null
+++ b/pgu/gui/textarea.py
@@ -0,0 +1,290 @@
+"""
+"""
+import pygame
+from pygame.locals import *
+
+from const import *
+import widget
+
+class TextArea(widget.Widget):
+ """A multi-line text input.
+
+ <pre>TextArea(value="",width = 120, height = 30, size=20)</pre>
+
+ <dl>
+ <dt>value<dd>initial text
+ <dt>size<dd>size for the text box, in characters
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ w = TextArea(value="Cuzco the Goat",size=20)
+
+ w = TextArea("Marbles")
+
+ w = TextArea("Groucho\nHarpo\nChico\nGummo\nZeppo\n\nMarx", 200, 400, 12)
+ </code>
+
+ """
+ def __init__(self,value="",width = 120, height = 30, size=20,**params):
+ params.setdefault('cls','input')
+ params.setdefault('width', width)
+ params.setdefault('height', height)
+
+ widget.Widget.__init__(self,**params)
+ self.value = value # The value of the TextArea
+ self.pos = len(str(value)) # The position of the cursor
+ self.vscroll = 0 # The number of lines that the TextArea is currently scrolled
+ self.font = self.style.font # The font used for rendering the text
+ self.cursor_w = 2 # Cursor width (NOTE: should be in a style)
+ w,h = self.font.size("e"*size)
+ if not self.style.height: self.style.height = h
+ if not self.style.width: self.style.width = w
+
+ def resize(self,width=None,height=None):
+ if (width != None) and (height != None):
+ self.rect = pygame.Rect(self.rect.x, self.rect.y, width, height)
+ return self.rect.w, self.rect.h
+
+ def paint(self,s):
+
+ # TODO: What's up with this 20 magic number? It's the margin of the left and right sides, but I'm not sure how this should be gotten other than by trial and error.
+ max_line_w = self.rect.w - 20
+
+ # Update the line allocation for the box's value
+ self.doLines(max_line_w)
+
+ # Make sure that the vpos and hpos of the cursor is set properly
+ self.updateCursorPos()
+
+ # Make sure that we're scrolled vertically such that the cursor is visible
+ if (self.vscroll < 0):
+ self.vscroll = 0
+ if (self.vpos < self.vscroll):
+ self.vscroll = self.vpos
+ elif ((self.vpos - self.vscroll + 1) * self.line_h > self.rect.h):
+ self.vscroll = - (self.rect.h / self.line_h - self.vpos - 1)
+
+ # Blit each of the lines in turn
+ cnt = 0
+ for line in self.lines:
+ line_pos = (0, (cnt - self.vscroll) * self.line_h)
+ if (line_pos[1] >= 0) and (line_pos[1] < self.rect.h):
+ s.blit( self.font.render(line, 1, self.style.color), line_pos )
+ cnt += 1
+
+ # If the textarea is focused, then also show the cursor
+ if self.container.myfocus is self:
+ r = self.getCursorRect()
+ s.fill(self.style.color,r)
+
+ # This function updates self.vpos and self.hpos based on self.pos
+ def updateCursorPos(self):
+ self.vpos = 0 # Reset the current line that the cursor is on
+ self.hpos = 0
+
+ line_cnt = 0
+ char_cnt = 0
+
+ for line in self.lines:
+ line_char_start = char_cnt # The number of characters at the start of the line
+
+ # Keep track of the character count for words
+ char_cnt += len(line)
+
+ # If our cursor count is still less than the cursor position, then we can update our cursor line to assume that it's at least on this line
+ if (char_cnt > self.pos):
+ self.vpos = line_cnt
+ self.hpos = self.pos - line_char_start
+
+ break # Now that we know where our cursor is, we exit the loop
+
+ line_cnt += 1
+
+ if (char_cnt <= self.pos) and (len(self.lines) > 0):
+ self.vpos = len(self.lines) - 1
+ self.hpos = len(self.lines[ self.vpos ] )
+
+ # Returns a rectangle that is of the size and position of where the cursor is drawn
+ def getCursorRect(self):
+ lw = 0
+ if (len(self.lines) > 0):
+ lw, lh = self.font.size( self.lines[ self.vpos ][ 0:self.hpos ] )
+
+ r = pygame.Rect(lw, (self.vpos - self.vscroll) * self.line_h, self.cursor_w, self.line_h)
+ return r
+
+ # This function sets the cursor position according to an x/y value (such as by from a mouse click)
+ def setCursorByXY(self, (x, y)):
+ self.vpos = ((int) (y / self.line_h)) + self.vscroll
+ if (self.vpos >= len(self.lines)):
+ self.vpos = len(self.lines) - 1
+
+ try:
+ currentLine = self.lines[ self.vpos ]
+ except IndexError:
+ currentLine = ''
+
+ for cnt in range(0, len(currentLine) ):
+ self.hpos = cnt
+ lw, lh = self.font.size( currentLine[ 0:self.hpos + 1 ] )
+ if (lw > x):
+ break
+
+ lw, lh = self.font.size( currentLine )
+ if (lw < x):
+ self.hpos = len(currentLine)
+
+ self.setCursorByHVPos()
+
+ # This function sets the cursor position by the horizontal/vertical cursor position.
+ def setCursorByHVPos(self):
+ line_cnt = 0
+ char_cnt = 0
+
+ for line in self.lines:
+ line_char_start = char_cnt # The number of characters at the start of the line
+
+ # Keep track of the character count for words
+ char_cnt += len(line)
+
+ # If we're on the proper line
+ if (line_cnt == self.vpos):
+ # Make sure that we're not trying to go over the edge of the current line
+ if ( self.hpos >= len(line) ):
+ self.hpos = len(line) - 1
+ # Set the cursor position
+ self.pos = line_char_start + self.hpos
+ break # Now that we've set our cursor position, we exit the loop
+
+ line_cnt += 1
+
+ # Splits up the text found in the control's value, and assigns it into the lines array
+ def doLines(self, max_line_w):
+ self.line_h = 10
+ self.lines = [] # Create an empty starter list to start things out.
+
+ inx = 0
+ line_start = 0
+ while inx >= 0:
+ # Find the next breakable whitespace
+ # HACK: Find a better way to do this to include tabs and system characters and whatnot.
+ prev_word_start = inx # Store the previous whitespace
+ spc_inx = self.value.find(' ', inx+1)
+ nl_inx = self.value.find('\n', inx+1)
+
+ if (min(spc_inx, nl_inx) == -1):
+ inx = max(spc_inx, nl_inx)
+ else:
+ inx = min(spc_inx, nl_inx)
+
+ # Measure the current line
+ lw, self.line_h = self.font.size( self.value[ line_start : inx ] )
+
+ # If we exceeded the max line width, then create a new line
+ if (lw > max_line_w):
+ #Fall back to the previous word start
+ self.lines.append(self.value[ line_start : prev_word_start + 1 ])
+ line_start = prev_word_start + 1
+ # TODO: Check for extra-long words here that exceed the length of a line, to wrap mid-word
+
+ # If we reached the end of our text
+ if (inx < 0):
+ # Then make sure we added the last of the line
+ if (line_start < len( self.value ) ):
+ self.lines.append( self.value[ line_start : len( self.value ) ] )
+ # If we reached a hard line break
+ elif (self.value[inx] == "\n"):
+ # Then make a line break here as well.
+ newline = self.value[ line_start : inx + 1 ]
+ newline = newline.replace("\n", " ") # HACK: We know we have a newline character, which doesn't print nicely, so make it into a space. Comment this out to see what I mean.
+ self.lines.append( newline )
+
+ line_start = inx + 1
+ else:
+ # Otherwise, we just continue progressing to the next space
+ pass
+
+ def _setvalue(self,v):
+ self.__dict__['value'] = v
+ self.send(CHANGE)
+
+ def event(self,e):
+ used = None
+ if e.type == KEYDOWN:
+ if e.key == K_BACKSPACE:
+ if self.pos:
+ self._setvalue(self.value[:self.pos-1] + self.value[self.pos:])
+ self.pos -= 1
+ elif e.key == K_DELETE:
+ if len(self.value) > self.pos:
+ self._setvalue(self.value[:self.pos] + self.value[self.pos+1:])
+ elif e.key == K_HOME:
+ # Find the previous newline
+ newPos = self.value.rfind('\n', 0, self.pos)
+ if (newPos >= 0):
+ self.pos = newPos
+ elif e.key == K_END:
+ # Find the previous newline
+ newPos = self.value.find('\n', self.pos, len(self.value) )
+ if (newPos >= 0):
+ self.pos = newPos
+ elif e.key == K_LEFT:
+ if self.pos > 0: self.pos -= 1
+ used = True
+ elif e.key == K_RIGHT:
+ if self.pos < len(self.value): self.pos += 1
+ used = True
+ elif e.key == K_UP:
+ self.vpos -= 1
+ self.setCursorByHVPos()
+ elif e.key == K_DOWN:
+ self.vpos += 1
+ self.setCursorByHVPos()
+ # The following return/tab keys are standard for PGU widgets, but I took them out here to facilitate multi-line text editing
+# elif e.key == K_RETURN:
+# self.next()
+# elif e.key == K_TAB:
+# pass
+ else:
+ #c = str(e.unicode)
+ try:
+ if (e.key == K_RETURN):
+ c = "\n"
+ elif (e.key == K_TAB):
+ c = " "
+ else:
+ c = (e.unicode).encode('latin-1')
+ if c:
+ self._setvalue(self.value[:self.pos] + c + self.value[self.pos:])
+ self.pos += len(c)
+ except: #ignore weird characters
+ pass
+ self.repaint()
+ elif e.type == MOUSEBUTTONDOWN:
+ self.setCursorByXY(e.pos)
+ self.repaint()
+
+ elif e.type == FOCUS:
+ self.repaint()
+ elif e.type == BLUR:
+ self.repaint()
+
+ self.pcls = ""
+ if self.container.myfocus is self: self.pcls = "focus"
+
+ return used
+
+ def __setattr__(self,k,v):
+ if k == 'value':
+ if v == None: v = ''
+ v = str(v)
+ self.pos = len(v)
+ _v = self.__dict__.get(k,NOATTR)
+ self.__dict__[k]=v
+ if k == 'value' and _v != NOATTR and _v != v:
+ self.send(CHANGE)
+ self.repaint()
+
+# The first version of this code was done by Clint Herron, and is a modified version of input.py (by Phil Hassey).
+# It is under the same license as the rest of the PGU library.
diff --git a/pgu/gui/theme.py b/pgu/gui/theme.py
new file mode 100644
index 0000000..c9345ce
--- /dev/null
+++ b/pgu/gui/theme.py
@@ -0,0 +1,473 @@
+"""
+"""
+import os, re
+import pygame
+
+from const import *
+import surface
+
+def _list_themes(dir):
+ d = {}
+ for entry in os.listdir(dir):
+ if os.path.exists(os.path.join(dir, entry, 'config.txt')):
+ d[entry] = os.path.join(dir, entry)
+ return d
+
+class Theme:
+ """Theme interface.
+
+ <p>If you wish to create your own theme, create a class with this interface, and
+ pass it to gui.App via <tt>gui.App(theme=MyTheme())</tt>.</p>
+
+ <strong>Default Theme</strong>
+
+ <pre>Theme(dirs='default')</pre>
+ <dl>
+ <dt>dirs<dd>Name of the theme dir to load a theme from. May be an absolute path to a theme, if pgu is not installed, or if you created your own theme. May include several dirs in a list if data is spread across several themes.
+ </dl>
+
+ <strong>Example</strong>
+
+ <code>
+ theme = gui.Theme("default")
+ theme = gui.Theme(["mytheme","mytheme2"])
+ </code>
+ """
+ def __init__(self,dirs='default'):
+ self.config = {}
+ self.dict = {}
+ self._loaded = []
+ self.cache = {}
+ self._preload(dirs)
+ pygame.font.init()
+
+ def _preload(self,ds):
+ if not isinstance(ds, list):
+ ds = [ds]
+ for d in ds:
+ if d not in self._loaded:
+ self._load(d)
+ self._loaded.append(d)
+
+ def _load(self, name):
+ #theme_dir = themes[name]
+
+ #try to load the local dir, or absolute path
+ dnames = [name]
+
+ #if the package isn't installed and people are just
+ #trying out the scripts or examples
+ dnames.append(os.path.join(os.path.dirname(__file__),"..","..","data","themes",name))
+
+ #if the package is installed, and the package is installed
+ #in /usr/lib/python2.3/site-packages/pgu/
+ #or c:\python23\lib\site-packages\pgu\
+ #the data is in ... lib/../share/ ...
+ dnames.append(os.path.join(os.path.dirname(__file__),"..","..","..","..","share","pgu","themes",name))
+ dnames.append(os.path.join(os.path.dirname(__file__),"..","..","..","..","..","share","pgu","themes",name))
+
+ for dname in dnames:
+ if os.path.isdir(dname): break
+ if not os.path.isdir(dname):
+ raise 'could not find theme '+name
+
+ fname = os.path.join(dname,"config.txt")
+ if os.path.isfile(fname):
+ try:
+ f = open(fname)
+ for line in f.readlines():
+ vals = line.strip().split()
+ if len(vals) < 3: continue
+ cls = vals[0]
+ del vals[0]
+ pcls = ""
+ if cls.find(":")>=0:
+ cls,pcls = cls.split(":")
+ attr = vals[0]
+ del vals[0]
+ self.config[cls+":"+pcls+" "+attr] = (dname, vals)
+ finally:
+ f.close()
+ fname = os.path.join(dname,"style.ini")
+ if os.path.isfile(fname):
+ import ConfigParser
+ cfg = ConfigParser.ConfigParser()
+ f = open(fname,'r')
+ cfg.readfp(f)
+ for section in cfg.sections():
+ cls = section
+ pcls = ''
+ if cls.find(":")>=0:
+ cls,pcls = cls.split(":")
+ for attr in cfg.options(section):
+ vals = cfg.get(section,attr).strip().split()
+ self.config[cls+':'+pcls+' '+attr] = (dname,vals)
+
+ is_image = re.compile('\.(gif|jpg|bmp|png|tga)$', re.I)
+ def _get(self,key):
+ if not key in self.config: return
+ if key in self.dict: return self.dict[key]
+ dvals = self.config[key]
+ dname, vals = dvals
+ #theme_dir = themes[name]
+ v0 = vals[0]
+ if v0[0] == '#':
+ v = pygame.color.Color(v0)
+ elif v0.endswith(".ttf") or v0.endswith(".TTF"):
+ v = pygame.font.Font(os.path.join(dname, v0),int(vals[1]))
+ elif self.is_image.search(v0) is not None:
+ v = pygame.image.load(os.path.join(dname, v0))
+ else:
+ try: v = int(v0)
+ except: v = pygame.font.SysFont(v0, int(vals[1]))
+ self.dict[key] = v
+ return v
+
+ def get(self,cls,pcls,attr):
+ """Interface method -- get the value of a style attribute.
+
+ <pre>Theme.get(cls,pcls,attr): return value</pre>
+
+ <dl>
+ <dt>cls<dd>class, for example "checkbox", "button", etc.
+ <dt>pcls<dd>pseudo class, for example "hover", "down", etc.
+ <dt>attr<dd>attribute, for example "image", "background", "font", "color", etc.
+ </dl>
+
+ <p>returns the value of the attribute.</p>
+
+ <p>This method is called from [[gui-style]].</p>
+ """
+
+ if not self._loaded: self._preload("default")
+
+ o = cls+":"+pcls+" "+attr
+
+ #if not hasattr(self,'_count'):
+ # self._count = {}
+ #if o not in self._count: self._count[o] = 0
+ #self._count[o] += 1
+
+ if o in self.cache:
+ return self.cache[o]
+
+ v = self._get(cls+":"+pcls+" "+attr)
+ if v:
+ self.cache[o] = v
+ return v
+
+ pcls = ""
+ v = self._get(cls+":"+pcls+" "+attr)
+ if v:
+ self.cache[o] = v
+ return v
+
+ cls = "default"
+ v = self._get(cls+":"+pcls+" "+attr)
+ if v:
+ self.cache[o] = v
+ return v
+
+ v = 0
+ self.cache[o] = v
+ return v
+
+ def box(self,w,s):
+ style = w.style
+
+ c = (0,0,0)
+ if style.border_color != 0: c = style.border_color
+ w,h = s.get_width(),s.get_height()
+
+ s.fill(c,(0,0,w,style.border_top))
+ s.fill(c,(0,h-style.border_bottom,w,style.border_bottom))
+ s.fill(c,(0,0,style.border_left,h))
+ s.fill(c,(w-style.border_right,0,style.border_right,h))
+
+
+ def getspacing(self,w):
+ # return the top, right, bottom, left spacing around the widget
+ if not hasattr(w,'_spacing'): #HACK: assume spacing doesn't change re pcls
+ s = w.style
+ xt = s.margin_top+s.border_top+s.padding_top
+ xr = s.padding_right+s.border_right+s.margin_right
+ xb = s.padding_bottom+s.border_bottom+s.margin_bottom
+ xl = s.margin_left+s.border_left+s.padding_left
+ w._spacing = xt,xr,xb,xl
+ return w._spacing
+
+
+ def resize(self,w,m):
+ def func(width=None,height=None):
+ #ww,hh = m(width,height)
+ ow,oh = width,height
+
+
+ s = w.style
+
+ pt,pr,pb,pl = s.padding_top,s.padding_right,s.padding_bottom,s.padding_left
+ bt,br,bb,bl = s.border_top,s.border_right,s.border_bottom,s.border_left
+ mt,mr,mb,ml = s.margin_top,s.margin_right,s.margin_bottom,s.margin_left
+
+ xt = pt+bt+mt
+ xr = pr+br+mr
+ xb = pb+bb+mb
+ xl = pl+bl+ml
+ ttw = xl+xr
+ tth = xt+xb
+
+ ww,hh = None,None
+ if width != None: ww = width-ttw
+ if height != None: hh = height-tth
+ ww,hh = m(ww,hh)
+
+ rect = pygame.Rect(0 + xl, 0 + xt, ww, hh)
+ w._rect_content = rect #pygame.Rect(0 + xl, 0 + xt, width, height)
+ #r = rect
+
+ if width == None: width = ww
+ if height == None: height = hh
+ #if the widget hasn't respected the style.width,
+ #style height, we'll add in the space for it...
+ width = max(width-ttw,ww,w.style.width)
+ height = max(height-tth,hh,w.style.height)
+
+ #width = max(ww,w.style.width-tw)
+ #height = max(hh,w.style.height-th)
+
+ r = pygame.Rect(rect.x,rect.y,width,height)
+
+ w._rect_padding = pygame.Rect(r.x-pl,r.y-pt,r.w+pl+pr,r.h+pt+pb)
+ r = w._rect_padding
+ w._rect_border = pygame.Rect(r.x-bl,r.y-bt,r.w+bl+br,r.h+bt+bb)
+ r = w._rect_border
+ w._rect_margin = pygame.Rect(r.x-ml,r.y-mt,r.w+ml+mr,r.h+mt+mb)
+
+ #align it within it's zone of power.
+ dx = width-rect.w
+ dy = height-rect.h
+ #rect.x += (1)*dx/2
+ #rect.y += (1)*dy/2
+ rect.x += (w.style.align+1)*dx/2
+ rect.y += (w.style.valign+1)*dy/2
+
+
+ #print w,ow, w._rect_margin.w, ttw
+ return w._rect_margin.w,w._rect_margin.h
+ return func
+
+
+ def paint(self,w,m):
+ def func(s):
+# if w.disabled:
+# if not hasattr(w,'_disabled_bkgr'):
+# w._disabled_bkgr = s.convert()
+# orig = s
+# s = w._disabled_bkgr.convert()
+
+# if not hasattr(w,'_theme_paint_bkgr'):
+# w._theme_paint_bkgr = s.convert()
+# else:
+# s.blit(w._theme_paint_bkgr,(0,0))
+#
+# if w.disabled:
+# orig = s
+# s = w._theme_paint_bkgr.convert()
+
+ if w.disabled:
+ if not (hasattr(w,'_theme_bkgr') and w._theme_bkgr.get_width() == s.get_width() and w._theme_bkgr.get_height() == s.get_height()):
+ w._theme_bkgr = s.copy()
+ orig = s
+ s = w._theme_bkgr
+ s.fill((0,0,0,0))
+ s.blit(orig,(0,0))
+
+ if hasattr(w,'background'):
+ w.background.paint(surface.subsurface(s,w._rect_border))
+ self.box(w,surface.subsurface(s,w._rect_border))
+ r = m(surface.subsurface(s,w._rect_content))
+
+ if w.disabled:
+ s.set_alpha(128)
+ orig.blit(s,(0,0))
+
+# if w.disabled:
+# orig.blit(w._disabled_bkgr,(0,0))
+# s.set_alpha(128)
+# orig.blit(s,(0,0))
+
+ w._painted = True
+ return r
+ return func
+
+ def event(self,w,m):
+ def func(e):
+ rect = w._rect_content
+ if e.type == MOUSEBUTTONUP or e.type == MOUSEBUTTONDOWN:
+ sub = pygame.event.Event(e.type,{
+ 'button':e.button,
+ 'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y)})
+ elif e.type == CLICK:
+ sub = pygame.event.Event(e.type,{
+ 'button':e.button,
+ 'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y)})
+ elif e.type == MOUSEMOTION:
+ sub = pygame.event.Event(e.type,{
+ 'buttons':e.buttons,
+ 'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y),
+ 'rel':e.rel})
+ else:
+ sub = e
+ r = m(sub)
+ return r
+ return func
+
+ def update(self,w,m):
+ def func(s):
+ if w.disabled: return []
+ r = m(surface.subsurface(s,w._rect_content))
+ if type(r) == list:
+ dx,dy = w._rect_content.topleft
+ for rr in r:
+ rr.x,rr.y = rr.x+dx,rr.y+dy
+ return r
+ return func
+
+ def open(self,w,m):
+ def func(widget=None,x=None,y=None):
+ if not hasattr(w,'_rect_content'): w.rect.w,w.rect.h = w.resize() #HACK: so that container.open won't resize again!
+ rect = w._rect_content
+ ##print w.__class__.__name__, rect
+ if x != None: x += rect.x
+ if y != None: y += rect.y
+ return m(widget,x,y)
+ return func
+
+ #def open(self,w,m):
+ # def func(widget=None):
+ # return m(widget)
+ # return func
+
+ def decorate(self,widget,level):
+ """Interface method -- decorate a widget.
+
+ <p>The theme system is given the opportunity to decorate a widget methods at the
+ end of the Widget initializer.</p>
+
+ <pre>Theme.decorate(widget,level)</pre>
+
+ <dl>
+ <dt>widget<dd>the widget to be decorated
+ <dt>level<dd>the amount of decoration to do, False for none, True for normal amount, 'app' for special treatment of App objects.
+ </dl>
+ """
+
+ w = widget
+ if level == False: return
+
+ if type(w.style.background) != int:
+ w.background = Background(w,self)
+
+ if level == 'app': return
+
+ for k,v in w.style.__dict__.items():
+ if k in ('border','margin','padding'):
+ for kk in ('top','bottom','left','right'):
+ setattr(w.style,'%s_%s'%(k,kk),v)
+
+ w.paint = self.paint(w,w.paint)
+ w.event = self.event(w,w.event)
+ w.update = self.update(w,w.update)
+ w.resize = self.resize(w,w.resize)
+ w.open = self.open(w,w.open)
+
+ def render(self,s,box,r):
+ """Interface method - render a special widget feature.
+
+ <pre>Theme.render(s,box,r)</pre>
+
+ <dl>
+ <dt>s<dt>pygame.Surface
+ <dt>box<dt>box data, a value returned from Theme.get, typically a pygame.Surface
+ <dt>r<dt>pygame.Rect with the size that the box data should be rendered
+ </dl>
+
+ """
+
+ if box == 0: return
+
+ if not isinstance(box,pygame.Surface):
+ s.fill(box,r)
+ return
+
+ x,y,w,h=r.x,r.y,r.w,r.h
+ ww,hh=box.get_width()/3,box.get_height()/3
+ xx,yy=x+w,y+h
+ src = pygame.rect.Rect(0,0,ww,hh)
+ dest = pygame.rect.Rect(0,0,ww,hh)
+
+ s.set_clip(pygame.Rect(x+ww,y+hh,w-ww*2,h-hh*2))
+ src.x,src.y = ww,hh
+ for dest.y in xrange(y+hh,yy-hh,hh):
+ for dest.x in xrange(x+ww,xx-ww,ww): s.blit(box,dest,src)
+
+ s.set_clip(pygame.Rect(x+ww,y,w-ww*3,hh))
+ src.x,src.y,dest.y = ww,0,y
+ for dest.x in xrange(x+ww,xx-ww*2,ww): s.blit(box,dest,src)
+ dest.x = xx-ww*2
+ s.set_clip(pygame.Rect(x+ww,y,w-ww*2,hh))
+ s.blit(box,dest,src)
+
+ s.set_clip(pygame.Rect(x+ww,yy-hh,w-ww*3,hh))
+ src.x,src.y,dest.y = ww,hh*2,yy-hh
+ for dest.x in xrange(x+ww,xx-ww*2,ww): s.blit(box,dest,src)
+ dest.x = xx-ww*2
+ s.set_clip(pygame.Rect(x+ww,yy-hh,w-ww*2,hh))
+ s.blit(box,dest,src)
+
+ s.set_clip(pygame.Rect(x,y+hh,xx,h-hh*3))
+ src.y,src.x,dest.x = hh,0,x
+ for dest.y in xrange(y+hh,yy-hh*2,hh): s.blit(box,dest,src)
+ dest.y = yy-hh*2
+ s.set_clip(pygame.Rect(x,y+hh,xx,h-hh*2))
+ s.blit(box,dest,src)
+
+ s.set_clip(pygame.Rect(xx-ww,y+hh,xx,h-hh*3))
+ src.y,src.x,dest.x=hh,ww*2,xx-ww
+ for dest.y in xrange(y+hh,yy-hh*2,hh): s.blit(box,dest,src)
+ dest.y = yy-hh*2
+ s.set_clip(pygame.Rect(xx-ww,y+hh,xx,h-hh*2))
+ s.blit(box,dest,src)
+
+ s.set_clip()
+ src.x,src.y,dest.x,dest.y = 0,0,x,y
+ s.blit(box,dest,src)
+
+ src.x,src.y,dest.x,dest.y = ww*2,0,xx-ww,y
+ s.blit(box,dest,src)
+
+ src.x,src.y,dest.x,dest.y = 0,hh*2,x,yy-hh
+ s.blit(box,dest,src)
+
+ src.x,src.y,dest.x,dest.y = ww*2,hh*2,xx-ww,yy-hh
+ s.blit(box,dest,src)
+
+
+
+import pygame
+import widget
+
+class Background(widget.Widget):
+ def __init__(self,value,theme,**params):
+ params['decorate'] = False
+ widget.Widget.__init__(self,**params)
+ self.value = value
+ self.theme = theme
+
+ def paint(self,s):
+ r = pygame.Rect(0,0,s.get_width(),s.get_height())
+ v = self.value.style.background
+ if not isinstance(v,pygame.Surface):
+ s.fill(v)
+ else:
+ self.theme.render(s,v,r)
diff --git a/pgu/gui/widget.py b/pgu/gui/widget.py
new file mode 100644
index 0000000..e1e970a
--- /dev/null
+++ b/pgu/gui/widget.py
@@ -0,0 +1,319 @@
+"""
+"""
+import pygame
+
+import style
+
+class Widget:
+ """Template object - base for all widgets.
+
+ <pre>Widget(**params)</pre>
+
+ <p>A number of optional params may be passed to the Widget initializer.</p>
+
+ <dl>
+ <dt>decorate<dd>defaults to True. If true, will call <tt>theme.decorate(self)</tt> to allow the theme a chance to decorate the widget.
+ <dt>style<dd>a dict of style parameters.
+ <dt>x, y, width, height<dd>position and size parameters, passed along to style
+ <dt>align, valign<dd>alignment parameters, passed along to style
+ <dt>font, color, background<dd>other common parameters that are passed along to style
+ <dt>cls<dd>class name as used by Theme
+ <dt>name<dd>name of widget as used by Form. If set, will call <tt>form.add(self,name)</tt> to add the widget to the most recently created Form.
+ <dt>focusable<dd>True if this widget can receive focus via Tab, etc. Defaults to True.
+ <dt>disabled<dd>True of this widget is disabled. Defaults to False.
+ <dt>value<dd>initial value
+ </dl>
+
+ <strong>Example - Creating your own Widget</strong>
+ <p>This example shows which methods are template methods.</p>
+ <code>
+ class Draw(gui.Widget):
+ def paint(self,s):
+ #paint the pygame.Surface
+ return
+
+ def update(self,s):
+ #update the pygame.Surface and return the update rects
+ return [pygame.Rect(0,0,self.rect.w,self.rect.h)]
+
+ def event(self,e):
+ #handle the pygame.Event
+ return
+
+ def resize(self,width=None,height=None):
+ #return the width and height of this widget
+ return 256,256
+ </code>
+ """
+
+ def __init__(self,**params):
+ #object.Object.__init__(self)
+ self.connects = {}
+ params.setdefault('decorate',True)
+ params.setdefault('style',{})
+ params.setdefault('focusable',True)
+ params.setdefault('disabled',False)
+
+ self.focusable = params['focusable']
+ self.disabled = params['disabled']
+
+ self.rect = pygame.Rect(params.get('x',0),params.get('y',0),params.get('width',0),params.get('height',0))
+
+ s = params['style']
+ #some of this is a bit "theme-ish" but it is very handy, so these
+ #things don't have to be put directly into the style.
+ for att in ('align','valign','x','y','width','height','color','font','background'):
+ if att in params: s[att] = params[att]
+ self.style = style.Style(self,s)
+
+ self.cls = 'default'
+ if 'cls' in params: self.cls = params['cls']
+ if 'name' in params:
+ import form
+ self.name = params['name']
+ if hasattr(form.Form,'form') and form.Form.form != None:
+ form.Form.form.add(self)
+ self.form = form.Form.form
+ if 'value' in params: self.value = params['value']
+ self.pcls = ""
+
+ if params['decorate'] != False:
+ import app
+ if not hasattr(app.App,'app'):
+ print 'gui.widget: creating an App'
+ app.App.app = app.App()
+ app.App.app.theme.decorate(self,params['decorate'])
+
+ def focus(self):
+ """Focus this Widget.
+
+ <pre>Widget.focus()</pre>
+ """
+ if getattr(self,'container',None) != None:
+ if self.container.myfocus != self: ## by Gal Koren
+ self.container.focus(self)
+ def blur(self):
+ """Blur this Widget.
+
+ <pre>Widget.blur()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.blur(self)
+ def open(self):
+ """Open this Widget as a modal dialog.
+
+ <pre>Widget.open()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.open(self)
+ def close(self):
+ """Close this Widget (if it is a modal dialog.)
+
+ <pre>Widget.close()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.close(self)
+ def resize(self,width=None,height=None):
+ """Template method - return the size and width of this widget.
+
+ <p>Responsible for also resizing all sub-widgets.</p>
+
+ <pre>Widget.resize(width,height): return width,height</pre>
+
+ <dl>
+ <dt>width<dd>suggested width
+ <dt>height<dd>suggested height
+ </dl>
+
+ <p>If not overridden, will return self.style.width, self.style.height</p>
+ """
+ return self.style.width, self.style.height
+ def chsize(self):
+ """Change the size of this widget.
+
+ <p>Calling this method will cause a resize on all the widgets,
+ including this one.</p>
+
+ <pre>Widget.chsize()</pre>
+ """
+
+ if not hasattr(self,'_painted'): return
+
+ if not hasattr(self,'container'): return
+ import app
+
+ if hasattr(app.App,'app'):
+ if app.App.app._chsize: return
+ app.App.app.chsize()
+ return
+
+ #if hasattr(app.App,'app'):
+ # w,h = self.rect.w,self.rect.h
+ # w2,h2 = self.resize()
+ # if w2 != w or h2 != h:
+ # app.App.app.chsize()
+ # else:
+ # self.repaint()
+
+
+ def update(self,s):
+ """Template method - update the surface
+
+ <pre>Widget.update(s): return list of pygame.Rect(s)</pre>
+
+ <dl>
+ <dt>s<dd>pygame.Surface to update
+ </dl>
+
+ <p>return - a list of the updated areas as pygame.Rect(s).</p>
+ """
+ return
+
+ def paint(self,s):
+ """Template method - paint the surface
+
+ <pre>Widget.paint(s)</pre>
+
+ <dl>
+ <dt>s<dd>pygame.Surface to paint
+ </dl>
+ """
+ return
+
+ def repaint(self):
+ """Request a repaint of this Widget.
+
+ <pre>Widget.repaint()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.repaint(self)
+ def repaintall(self):
+ """Request a repaint of all Widgets.
+
+ <pre>Widget.repaintall()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.repaintall()
+ def reupdate(self):
+ """Request a reupdate of this Widget
+
+ <pre>Widget.reupdate()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.reupdate(self)
+ def next(self):
+ """Pass focus to next Widget.
+
+ <p>Widget order determined by the order they were added to their container.</p>
+
+ <pre>Widget.next()</pre>
+ """
+ if getattr(self,'container',None) != None: self.container.next(self)
+ def previous(self):
+ """Pass focus to previous Widget.
+
+ <p>Widget order determined by the order they were added to their container.</p>
+
+ <pre>Widget.previous()</pre>
+ """
+
+ if getattr(self,'container',None) != None: self.container.previous(self)
+
+ def get_abs_rect(self):
+ """Get the absolute rect of this widget on the App screen
+
+ <pre>Widget.get_abs_rect(): return pygame.Rect</pre>
+ """
+ x, y = self.rect.x , self.rect.y
+ x += self._rect_content.x
+ y += self._rect_content.y
+ c = getattr(self,'container',None)
+ while c:
+ x += c.rect.x
+ y += c.rect.y
+ if hasattr(c,'_rect_content'):
+ x += c._rect_content.x
+ y += c._rect_content.y
+ c = getattr(c,'container',None)
+ return pygame.Rect(x, y, self.rect.w, self.rect.h)
+
+ def connect(self,code,fnc,*values):
+ """Connect a event code to a callback function.
+
+ <p>There may only be one callback per event code.</p>
+
+ <pre>Object.connect(code,fnc,value)</pre>
+
+ <dl>
+ <dt>code<dd>event type [[gui-const]]
+ <dt>fnc<dd>callback function
+ <dt>*values<dd>values to pass to callback. Please note that callbacks may also have "magicaly" parameters. Such as:
+ <dl>
+ <dt>_event<dd>receive the event
+ <dt>_code<dd>receive the event code
+ <dt>_widget<dd>receive the sending widget
+ </dl>
+ </dl>
+
+ <strong>Example</strong>
+ <code>
+ def onclick(value):
+ print 'click',value
+
+ w = Button("PGU!")
+ w.connect(gui.CLICK,onclick,'PGU Button Clicked')
+ </code>
+ """
+
+ self.connects[code] = {'fnc':fnc,'values':values}
+
+ def send(self,code,event=None):
+ """Send a code, event callback trigger.
+
+ <pre>Object.send(code,event=None)</pre>
+
+ <dl>
+ <dt>code<dd>event code
+ <dt>event<dd>event
+ </dl>
+ """
+ if code in self.connects:
+ con = self.connects[code]
+ #con['fnc'](*con['values'])
+
+ fnc = con['fnc']
+ values = list(con['values'])
+
+ nargs = fnc.func_code.co_argcount
+ names = list(fnc.func_code.co_varnames)[:nargs]
+ if hasattr(fnc,'im_class'): names.pop(0)
+
+ args = []
+ magic = {'_event':event,'_code':code,'_widget':self}
+ for name in names:
+ if name in magic.keys():
+ args.append(magic[name])
+ elif len(values):
+ args.append(values.pop(0))
+ else:
+ break
+ args.extend(values)
+ fnc(*args)
+
+ def _event(self,e):
+ if self.disabled: return
+ self.send(e.type,e)
+ return self.event(e)
+# return
+# import app
+# if hasattr(app.App,'app'):
+# app.App.app.events.append((self,e))
+
+ def event(self,e):
+ """Template method - called when an event is passed to this object.
+
+ <p>Please note that if you use an event, returning the value True
+ will stop parent containers from also using the event. (For example, if
+ your widget handles TABs or arrow keys, and you don't want those to
+ also alter the focus.)</p>
+
+ <dl>
+ <dt>e<dd>event
+ </dl>
+ """
+
+ return
diff --git a/pgu/hexvid.py b/pgu/hexvid.py
new file mode 100644
index 0000000..2d4156d
--- /dev/null
+++ b/pgu/hexvid.py
@@ -0,0 +1,127 @@
+"""Hexagonal tile engine.
+
+<p>Note -- this engine is not finished. Sprites are not supported. It
+can still be useful for using the level editor, and for rendering hex
+terrains, however. If you are able to update it and use it in a real game,
+help would be greatly appreciated!</p>
+
+<p>please note that this file is alpha, and is subject to modification in
+future versions of pgu!</p>
+
+"""
+print 'pgu.hexvid','This module is alpha, and is subject to change.'
+
+from pgu.vid import *
+import pygame
+
+
+class Hexvid(Vid):
+ """Create an hex vid engine. See [[vid]]"""
+ def update(self,screen):
+ return self.paint(screen)
+
+ def paint(self,screen):
+ sw,sh = screen.get_width(),screen.get_height()
+ self.view.w,self.view.h = sw,sh
+
+ tlayer = self.tlayer
+ blayer = self.blayer
+ #zlayer = self.zlayer
+ w,h = len(tlayer[0]),len(tlayer)
+
+ #iso_w,iso_h,iso_z,tile_w,tile_h,base_w,base_h = self.iso_w,self.iso_h,self.iso_z,self.tile_w,self.tile_h,self.base_w,self.base_h
+
+ tile_w,tile_h = self.tile_w,self.tile_h
+ tile_w2,tile_h2 = tile_w/2,tile_h/2
+
+ view = self.view
+ adj = self.adj = pygame.Rect(-self.view.x,-self.view.y,0,0)
+
+ w,h = len(tlayer[0]),len(tlayer)
+ tiles = self.tiles
+
+ #""
+ if self.bounds == None:
+ tmp,y1 = self.tile_to_view((0,0))
+ x1,tmp = self.tile_to_view((0,h+1))
+ tmp,y2 = self.tile_to_view((w+1,h+1))
+ x2,tmp = self.tile_to_view((w+1,0))
+ self.bounds = pygame.Rect(x1,y1,x2-x1,y2-y1)
+ print self.bounds
+ #""
+
+ if self.bounds != None: self.view.clamp_ip(self.bounds)
+
+ ox,oy = self.screen_to_tile((0,0))
+ sx,sy = self.tile_to_view((ox,oy))
+ dx,dy = sx - self.view.x,sy - self.view.y
+
+ bot = 1
+
+ tile_wi = tile_w + tile_w/2
+ tile_wi2 = tile_wi/2
+
+ #dx += tile_w/2
+
+ for i2 in xrange(-bot,self.view.h/tile_h2+bot*3): #NOTE: 3 seems a bit much, but it works.
+ tx,ty = ox + i2/2 + i2%2,oy + i2/2
+ x,y = (i2%2)*tile_wi2 + dx,i2*tile_h2 + dy
+
+ #to adjust for the -1 in i1
+ x,tx,ty = x-tile_wi,tx-1,ty+1
+
+ x -= tile_w/2
+ for i1 in xrange(-1,self.view.w/tile_wi+1):
+ if ty >= 0 and ty < h and tx >= 0 and tx < w:
+ if blayer != None:
+ n = blayer[ty][tx]
+ if n != 0:
+ t = tiles[n]
+ if t != None and t.image != None:
+ screen.blit(t.image,(x,y))
+ n = tlayer[ty][tx]
+ if n != 0:
+ t = tiles[n]
+ if t != None and t.image != None:
+ screen.blit(t.image,(x,y))
+
+
+ tx += 1
+ ty -= 1
+ x += tile_wi
+
+ return [pygame.Rect(0,0,screen.get_width(),screen.get_height())]
+
+ def view_to_tile(self,pos):
+ x,y = pos
+ #x = x + (self.tile_w*1/2)
+
+ x,y = int(x*4/(self.tile_w*3)), y*2/self.tile_h
+ nx = (x + y) / 2
+ ny = (y - x) / 2
+ return nx,ny
+
+ def tile_to_view(self,pos):
+ x,y = pos
+ nx = x - y
+ ny = x + y
+ nx,ny = int(nx*(self.tile_w*3)/4), ny*self.tile_h/2
+
+ #nx = nx - (self.tile_w*1/2)
+ return nx,ny
+
+ def screen_to_tile(self,pos): #NOTE HACK : not sure if the 3/8 is right or not, but it is pretty close...
+ pos = pos[0]+self.view.x + self.tile_w*3/8,pos[1]+self.view.y
+ pos = self.view_to_tile(pos)
+ return pos
+
+ def tile_to_screen(self,pos):
+ pos = self.tile_to_view(pos)
+ pos = pos[0]-self.view.x,pos[1]-self.view.y
+ return pos
+
+
+ def tga_load_tiles(self,fname,size,tdata={}):
+ Vid.tga_load_tiles(self,fname,size,tdata)
+
+ self.tile_w,self.tile_h = size \ No newline at end of file
diff --git a/pgu/high.py b/pgu/high.py
new file mode 100644
index 0000000..e05d22a
--- /dev/null
+++ b/pgu/high.py
@@ -0,0 +1,154 @@
+"""Classes for handling high score tables.
+"""
+
+import os
+
+def High(fname,limit=10):
+ """Create a Highs object and returns the default high score table.
+
+ <pre>High(fname,limit=10)</pre>
+
+ <dl>
+ <dt>fname <dd>filename to store high scores in
+ <dt>limit <dd>limit of scores to be recorded, defaults to 10
+ </dl>
+ """
+ return Highs(fname,limit)['default']
+
+class _Score:
+ def __init__(self,score,name,data=None):
+ self.score,self.name,self.data=score,name,data
+
+class _High:
+ """A high score table. These objects are passed to the user, but should not be created directly.
+
+ <p>You can iterate them:</p>
+ <code>
+ for e in myhigh:
+ print e.score,e.name,e.data
+ </code>
+
+ <p>You can modify them:</p>
+ <code>
+ myhigh[0].name = 'Cuzco'
+ </code>
+
+ <p>You can find out their length:</p>
+ <code>
+ print len(myhigh)
+ </code>
+ """
+
+ def __init__(self,highs,limit=10):
+ self.highs = highs
+ self._list = []
+ self.limit = limit
+
+ def save(self):
+ """Save the high scores.
+
+ <pre>_High.save()</pre>
+ """
+ self.highs.save()
+
+ def submit(self,score,name,data=None):
+ """Submit a high score to this table.
+
+ <pre>_High.submit(score,name,data=None)</pre>
+
+ <p>return -- the position in the table that the score attained. None if the score did not attain a position in the table.</p>
+ """
+ n = 0
+ for e in self._list:
+ if score > e.score:
+ self._list.insert(n,_Score(score,name,data))
+ self._list = self._list[0:self.limit]
+ return n
+ n += 1
+ if len(self._list) < self.limit:
+ self._list.append(_Score(score,name,data))
+ return len(self._list)-1
+
+ def check(self,score):
+ """Check if a score will attain a position in the table.
+
+ <pre>_High.check(score)</pre>
+
+ <p>return -- the position the score will attain, else None</p>
+ """
+ n = 0
+ for e in self._list:
+ if score > e.score:
+ return n
+ n += 1
+ if len(self._list) < self.limit:
+ return len(self._list)
+
+
+ def __iter__(self):
+ return self._list.__iter__()
+
+ def __getitem__(self,key):
+ return self._list[key]
+
+ def __len__(self):
+ return self._list.__len__()
+
+
+class Highs:
+ """The high score object.
+
+ <pre>Highs(fname,limit=10)</pre>
+ <ul>
+ <dt>fname <dd>filename to store high scores in
+ <dt>limit <dd>limit of scores to be recorded, defaults to 10
+ </ul>
+
+ <p>You may access _High objects through this object:</p>
+
+ <code>
+ my_easy_hs = highs['easy']
+ my_hard_hs = highs['hard']
+ </code>
+
+ """
+ def __init__(self,fname,limit=10):
+ self.fname = fname
+ self.limit = limit
+ self.load()
+
+ def load(self):
+ """Re-load the high scores.
+
+ <pre>Highs.load()</pre>
+ """
+
+ self._dict = {}
+ try:
+ f = open(self.fname)
+ for line in f.readlines():
+ key,score,name,data = line.strip().split("\t")
+ if key not in self._dict:
+ self._dict[key] = _High(self,self.limit)
+ high = self._dict[key]
+ high.submit(int(score),name,data)
+ f.close()
+ except:
+ pass
+
+ def save(self):
+ """Save the high scores.
+
+ <pre>Highs.save()</pre>
+ """
+
+ f = open(self.fname,"w")
+ for key,high in self._dict.items():
+ for e in high:
+ f.write("%s\t%d\t%s\t%s\n"%(key,e.score,e.name,str(e.data)))
+ f.close()
+
+ def __getitem__(self,key):
+ if key not in self._dict:
+ self._dict[key] = _High(self,self.limit)
+ return self._dict[key]
diff --git a/pgu/html.py b/pgu/html.py
new file mode 100644
index 0000000..40b9da3
--- /dev/null
+++ b/pgu/html.py
@@ -0,0 +1,536 @@
+"""a html renderer
+"""
+import htmllib
+import re
+import pygame
+from pygame.locals import *
+
+from pgu import gui
+
+_amap = {'left':-1,'right':1,'center':0,None:None,'':None,}
+_vamap = {'top':-1,'bottom':1,'center':0,'middle':0,None:None,'':None,}
+
+class _dummy:
+ pass
+
+class _flush:
+ def __init__(self):
+ self.style = _dummy()
+ self.style.font = None
+ self.style.color = None
+ self.cls = None
+ def add(self,w): pass
+ def space(self,v): pass
+
+class _hr(gui.Color):
+ def __init__(self,**params):
+ gui.Color.__init__(self,(0,0,0),**params)
+ def resize(self,width=None,height=None):
+ w,h = self.style.width,self.style.height
+ #if width != None: self.rect.w = width
+ #else: self.rect.w = 1
+
+ #xt,xr,xb,xl = self.getspacing()
+
+ if width != None: w = max(w,width)
+ if height != None: h = max(h,height)
+ w = max(w,1)
+ h = max(h,1)
+
+ return w,h #self.container.rect.w,h
+
+ #self.rect.w = max(1,width,self.container.rect.w-(xl+xr))
+
+ #print self.rect
+ #self.rect.w = 1
+
+class _html(htmllib.HTMLParser):
+ def init(self,doc,font,color,_globals,_locals):
+ self.mystack = []
+ self.document = doc
+ self.myopen('document',self.document)
+
+ self.myfont = self.font = font
+ self.mycolor = self.color = color
+
+ self.form = None
+
+ self._globals = _globals
+ self._locals = _locals
+
+ def myopen(self,type_,w):
+
+ self.mystack.append((type_,w))
+ self.type,self.item = type_,w
+
+ self.font = self.item.style.font
+ self.color = self.item.style.color
+
+ if not self.font: self.font = self.myfont
+ if not self.color: self.color = self.mycolor
+
+ def myclose(self,type_):
+ t = None
+ self.mydone()
+ while t != type_:
+ #if len(self.mystack)==0: return
+ t,w = self.mystack.pop()
+ t,w = self.mystack.pop()
+ self.myopen(t,w)
+
+ def myback(self,type_):
+ if type(type_) == str: type_ = [type_,]
+ self.mydone()
+ #print 'myback',type_
+ t = None
+ while t not in type_:
+ #if len(self.mystack)==0: return
+ t,w = self.mystack.pop()
+ self.myopen(t,w)
+
+ def mydone(self):
+ #clearing out the last </p>
+ if not hasattr(self.item,'layout'): return
+ if len(self.item.layout._widgets) == 0: return
+ w = self.item.layout._widgets[-1]
+ if type(w) == tuple:
+ del self.item.layout._widgets[-1]
+
+
+ def start_b(self,attrs): self.font.set_bold(1)
+ def end_b(self): self.font.set_bold(0)
+ def start_i(self,attrs): self.font.set_italic(1)
+ def end_i(self): self.font.set_italic(0)
+ def start_u(self,attrs): self.font.set_underline(1)
+ def end_u(self): self.font.set_underline(0)
+ def start_br(self,attrs): self.do_br(attrs)
+ def do_br(self,attrs): self.item.br(self.font.size(" ")[1])
+ def attrs_to_map(self,attrs):
+ k = None
+ r = {}
+ for k,v in attrs: r[k] = v
+ return r
+
+ def map_to_params(self,r):
+ anum = re.compile("\D")
+
+ params = {'style':{}}
+ style = params['style']
+
+ if 'bgcolor' in r: style['background'] = pygame.Color(r['bgcolor'])
+ if 'background' in r: style['background'] = pygame.image.load(r['background'])
+ if 'border' in r: style['border'] = int(r['border'])
+
+ for k in ['width','height','colspan','rowspan','size','min','max']:
+ if k in r: params[k] = int(anum.sub("",r[k]))
+
+ for k in ['name','value']:
+ if k in r: params[k] = r[k]
+
+ if 'class' in r: params['cls'] = r['class']
+
+ if 'align' in r:
+ params['align'] = _amap[r['align']]
+ if 'valign' in r:
+ params['valign'] = _vamap[r['valign']]
+
+ if 'style' in r:
+ for st in r['style'].split(";"):
+ #print st
+ if ":" in st:
+ #print st.split(":")
+ k,v = st.split(":")
+ k = k.replace("-","_")
+ k = k.replace(" ","")
+ v = v.replace(" ","")
+ if k == 'color' or k == 'border_color' or k == 'background':
+ v = pygame.Color(v)
+ else:
+ v = int(anum.sub("",v))
+ style[k] = v
+ return params
+
+ def map_to_connects(self,e,r):
+ for k,evt in [('onclick',gui.CLICK),('onchange',gui.CHANGE)]: #blah blah blah
+
+ if k in r:
+ #print k,r[k]
+ e.connect(evt,self.myexec,(e,r[k]))
+
+ def start_p(self,attrs):
+ r = self.attrs_to_map(attrs)
+ align = r.get("align","left")
+
+ self.check_p()
+ self.item.block(_amap[align])
+
+ def check_p(self):
+ if len(self.item.layout._widgets) == 0: return
+ if type(self.item.layout._widgets[-1]) == tuple:
+ w,h = self.item.layout._widgets[-1]
+ if w == 0: return
+ self.do_br(None)
+
+ def end_p(self):
+ #print 'end p'
+ self.check_p()
+
+
+ def start_block(self,t,attrs,align=-1):
+ r = self.attrs_to_map(attrs)
+ params = self.map_to_params(r)
+ if 'cls' in params: params['cls'] = t+"."+params['cls']
+ else: params['cls'] = t
+ b = gui.Document(**params)
+ if 'align' in params:
+ align = params['align']
+ self.item.block(align)
+ self.item.add(b)
+ self.myopen(t,b)
+
+
+
+ def end_block(self,t):
+ self.myclose(t)
+ self.item.block(-1)
+
+ def start_div(self,attrs): self.start_block('div',attrs)
+ def end_div(self): self.end_block('div')
+ def start_center(self,attrs): self.start_block('div',attrs,0)
+ def end_center(self): self.end_block('div')
+
+ def start_h1(self,attrs): self.start_block('h1',attrs)
+ def end_h1(self): self.end_block('h1')
+ def start_h2(self,attrs): self.start_block('h2',attrs)
+ def end_h2(self): self.end_block('h2')
+ def start_h3(self,attrs): self.start_block('h3',attrs)
+ def end_h3(self): self.end_block('h3')
+ def start_h4(self,attrs): self.start_block('h4',attrs)
+ def end_h4(self): self.end_block('h4')
+ def start_h5(self,attrs): self.start_block('h5',attrs)
+ def end_h5(self): self.end_block('h5')
+ def start_h6(self,attrs): self.start_block('h6',attrs)
+ def end_h6(self): self.end_block('h6')
+
+ def start_ul(self,attrs): self.start_block('ul',attrs)
+ def end_ul(self): self.end_block('ul')
+ def start_ol(self,attrs):
+ self.start_block('ol',attrs)
+ self.item.counter = 0
+ def end_ol(self): self.end_block('ol')
+ def start_li(self,attrs):
+ self.myback(['ul','ol'])
+ cur = self.item
+ self.start_block('li',attrs)
+ if hasattr(cur,'counter'):
+ cur.counter += 1
+ self.handle_data("%d. "%cur.counter)
+ else:
+ self.handle_data("- ")
+ #def end_li(self): self.end_block('li') #this isn't needed because of how the parser works
+
+ def start_pre(self,attrs): self.start_block('pre',attrs)
+ def end_pre(self): self.end_block('pre')
+ def start_code(self,attrs): self.start_block('code',attrs)
+ def end_code(self): self.end_block('code')
+
+ def start_table(self,attrs):
+ r = self.attrs_to_map(attrs)
+ params = self.map_to_params(r)
+
+ align = r.get("align","left")
+ self.item.block(_amap[align])
+
+ t = gui.Table(**params)
+ self.item.add(t)
+
+ self.myopen('table',t)
+
+ def start_tr(self,attrs):
+ self.myback('table')
+ self.item.tr()
+
+ def _start_td(self,t,attrs):
+ r = self.attrs_to_map(attrs)
+ params = self.map_to_params(r)
+ if 'cls' in params: params['cls'] = t+"."+params['cls']
+ else: params['cls'] = t
+ b = gui.Document(cls=t)
+
+ self.myback('table')
+ self.item.td(b,**params)
+ self.myopen(t,b)
+
+ self.font = self.item.style.font
+ self.color = self.item.style.color
+
+ def start_td(self,attrs):
+ self._start_td('td',attrs)
+
+ def start_th(self,attrs):
+ self._start_td('th',attrs)
+
+ def end_table(self):
+ self.myclose('table')
+ self.item.block(-1)
+
+ def start_form(self,attrs):
+ r = self.attrs_to_map(attrs)
+ e = self.form = gui.Form()
+ e.groups = {}
+
+ self._locals[r.get('id',None)] = e
+
+ def start_input(self,attrs):
+ r = self.attrs_to_map(attrs)
+ params = self.map_to_params(r) #why bother
+ #params = {}
+
+ type_,name,value = r.get('type','text'),r.get('name',None),r.get('value',None)
+ f = self.form
+ if type_ == 'text':
+ e = gui.Input(**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ elif type_ == 'radio':
+ if name not in f.groups:
+ f.groups[name] = gui.Group(name=name)
+ g = f.groups[name]
+ del params['name']
+ e = gui.Radio(group=g,**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ if 'checked' in r: g.value = value
+ elif type_ == 'checkbox':
+ if name not in f.groups:
+ f.groups[name] = gui.Group(name=name)
+ g = f.groups[name]
+ del params['name']
+ e = gui.Checkbox(group=g,**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ if 'checked' in r: g.value = value
+
+ elif type_ == 'button':
+ e = gui.Button(**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ elif type_ == 'submit':
+ e = gui.Button(**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ elif type_ == 'file':
+ e = gui.Input(**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ b = gui.Button(value='Browse...')
+ self.item.add(b)
+ def _browse(value):
+ d = gui.FileDialog();
+ d.connect(gui.CHANGE,gui.action_setvalue,(d,e))
+ d.open();
+ b.connect(gui.CLICK,_browse,None)
+
+ self._locals[r.get('id',None)] = e
+
+ def start_object(self,attrs):
+ r = self.attrs_to_map(attrs)
+ params = self.map_to_params(r)
+ code = "e = %s(**params)"%r['type']
+ #print code
+ #print params
+ exec(code)
+ #print e
+ #print e.style.width,e.style.height
+ self.map_to_connects(e,r)
+ self.item.add(e)
+
+ self._locals[r.get('id',None)] = e
+
+ def start_select(self,attrs):
+ r = self.attrs_to_map(attrs)
+ params = {}
+
+ name,value = r.get('name',None),r.get('value',None)
+ e = gui.Select(name=name,value=value,**params)
+ self.map_to_connects(e,r)
+ self.item.add(e)
+ self.myopen('select',e)
+
+ def start_option(self,attrs):
+ r = self.attrs_to_map(attrs)
+ params = {} #style = self.map_to_style(r)
+
+ self.myback('select')
+ e = gui.Document(**params)
+ self.item.add(e,value=r.get('value',None))
+ self.myopen('option',e)
+
+
+ def end_select(self):
+ self.myclose('select')
+
+ def start_hr(self,attrs):
+ self.do_hr(attrs)
+ def do_hr(self,attrs):
+ h = self.font.size(" ")[1]/2
+
+ r = self.attrs_to_map(attrs)
+ params = self.map_to_params(r)
+ params['style']['padding'] = h
+ print params
+
+ self.item.block(0)
+ self.item.add(_hr(**params))
+ self.item.block(-1)
+
+ def anchor_begin(self,href,name,type_):
+ pass
+
+ def anchor_end(self):
+ pass
+
+ def start_title(self,attrs): self.myopen('title',_flush())
+ def end_title(self): self.myclose('title')
+
+ def myexec(self,value):
+ w,code = value
+ g = self._globals
+ l = self._locals
+ l['self'] = w
+ exec(code,g,l)
+
+ def handle_image(self,src,alt,ismap,align,width,height):
+ try:
+ w = gui.Image(pygame.image.load(src))
+ if align != '':
+ self.item.add(w,_amap[align])
+ else:
+ self.item.add(w)
+ except:
+ print 'handle_image: missing %s'%src
+
+ def handle_data(self,txt):
+ if self.type == 'table': return
+ elif self.type in ('pre','code'):
+ txt = txt.replace("\t"," ")
+ ss = txt.split("\n")
+ if ss[-1] == "": del ss[-1]
+ for sentence in ss:
+ img = self.font.render(sentence,1,self.color)
+ w = gui.Image(img)
+ self.item.add(w)
+ self.item.block(-1)
+ return
+
+ txt = re.compile("^[\t\r\n]+").sub("",txt)
+ txt = re.compile("[\t\r\n]+$").sub("",txt)
+
+ tst = re.compile("[\t\r\n]+").sub("",txt)
+ if tst == "": return
+
+ txt = re.compile("\s+").sub(" ",txt)
+ if txt == "": return
+
+ if txt == " ":
+ self.item.space(self.font.size(" "))
+ return
+
+ for word in txt.split(" "):
+ word = word.replace(chr(160)," ") #&nbsp;
+ #print self.item.cls
+ w = gui.Image(self.font.render(word,1,self.color))
+ self.item.add(w)
+ self.item.space(self.font.size(" "))
+
+
+class HTML(gui.Document):
+ """a gui HTML object
+
+ <pre>HTML(data,globals=None,locals=None)</pre>
+
+ <dl>
+ <dt>data <dd>html data
+ <dt>globals <dd>global variables (for scripting)
+ <dt>locals <dd>local variables (for scripting)
+ </dl>
+
+ <p>you may access html elements that have an id via widget[id]</p>
+ """
+ def __init__(self,data,globals=None,locals=None,**params):
+ gui.Document.__init__(self,**params)
+
+ _globals,_locals = globals,locals
+
+ if _globals == None: _globals = {}
+ if _locals == None: _locals = {}
+ self._globals = _globals
+ self._locals = _locals
+
+ #font = gui.theme.get("label","","font")
+ p = _html(htmllib.AS_IS,0)
+ p.init(self,self.style.font,self.style.color,_globals,_locals)
+ p.feed(data)
+ p.close()
+ p.mydone()
+
+
+ def __getitem__(self,k):
+ return self._locals[k]
+
+def render(font,rect,text,aa,color,bgcolor=(0,0,0,0)):
+ """render some html
+
+ <pre>render(font,rect,text,aa,color,bgcolor=(0,0,0,0))</pre>
+ """
+ fnt,r,txt,a,fg,bg = font,rect,text,aa,color,bgcolor
+
+ e = HTML(txt,font=fnt,color=fg)
+ e.resize(width=rect.w)
+ s = pygame.Surface((e.rect.w,e.rect.h),SWSURFACE|SRCALPHA,32)
+ s.fill(bg)
+ e.paint(s)
+
+ return s
+
+def rendertrim(font,rect,text,aa,color,bgcolor=(0,0,0,0)):
+ """render html, and make sure to trim the size
+
+ <pre>rendertrim(font,rect,text,aa,color,bgcolor=(0,0,0,0))</pre>
+ """
+ fnt,r,txt,a,fg,bg = font,rect,text,aa,color,bgcolor
+ #print r
+ w = HTML(txt,font=fnt,color=fg)
+ w.resize(width=rect.w)
+ s = pygame.Surface((w.rect.w,w.rect.h),SWSURFACE|SRCALPHA,32)
+ s.fill(bg)
+ w.paint(s)
+
+ minx,miny,maxx,maxy = 1024,1024,-1024,-1024
+ for e in w.layout.widgets:
+ x,y,w,h = e.rect.x,e.rect.y,e.rect.w,e.rect.h
+ minx = min(minx,x)
+ miny = min(miny,y)
+ x,y = x+w,y+h
+ maxx = max(maxx,x)
+ maxy = max(maxy,y)
+
+ r = pygame.Rect(minx,miny,maxx-minx,maxy-miny)
+
+ return s.subsurface((r))
+
+
+def write(s,font,rect,text,aa=0,color=(0,0,0)):
+ """write html to a surface
+
+ <pre>write(s,font,rect,text,aa=0,color=(0,0,0))</pre>
+ """
+ fnt,r,txt,a,fg = font,rect,text,aa,color
+
+ e = HTML(txt)
+
+ e.resize(width=rect.w)
+ s = s.subsurface(rect)
+ e.paint(s)
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/isovid.py b/pgu/isovid.py
new file mode 100644
index 0000000..d5048ec
--- /dev/null
+++ b/pgu/isovid.py
@@ -0,0 +1,182 @@
+"""Isometric tile engine.
+
+<p>Note -- this engine is not finished, any may not work for your
+particular needs. If you are able to update it, help would be
+greatly appreciated!</p>
+
+<p>please note that this file is alpha, and is subject to modification in
+future versions of pgu!</p>
+
+"""
+print 'pgu.isovid','This module is alpha, and is subject to change.'
+
+from pgu.vid import *
+import pygame
+
+class Isovid(Vid):
+ """Create an iso vid engine. See [[vid]]"""
+ def update(self,screen):
+ return self.paint(screen)
+
+ def paint(self,screen):
+ sw,sh = screen.get_width(),screen.get_height()
+
+ tlayer = self.tlayer
+ blayer = self.blayer
+ zlayer = self.zlayer
+ w,h = len(tlayer[0]),len(tlayer)
+
+ iso_w,iso_h,iso_z,tile_w,tile_h,base_w,base_h = self.iso_w,self.iso_h,self.iso_z,self.tile_w,self.tile_h,self.base_w,self.base_h
+
+ base_h2 = base_h/2
+ base_w2 = base_w/2
+
+ bot = tile_h/base_h2
+ todo_max = sh/base_h2+bot
+ todo = [[] for y in xrange(0,todo_max)]
+
+ self.view.w,self.view.h = sw,sh
+ view = self.view
+ adj = self.adj = pygame.Rect(-self.view.x,-self.view.y,0,0)
+
+ for s in self.sprites:
+ self.sprite_calc_irect(s)
+ x,y = self.iso_to_view((s.rect.centerx,s.rect.centery))
+ v = (y+adj.y)/base_h2 - 1
+ if v >= 0 and v < todo_max:
+ todo[v].append((s.image,s.irect))
+ #else: print 'doesnt fit',v
+
+ w,h = len(tlayer[0]),len(tlayer)
+ tiles = self.tiles
+
+ #""
+ if self.bounds == None:
+ tmp,y1 = self.tile_to_view((0,0))
+ x1,tmp = self.tile_to_view((0,h+1))
+ tmp,y2 = self.tile_to_view((w+1,h+1))
+ x2,tmp = self.tile_to_view((w+1,0))
+ self.bounds = pygame.Rect(x1,y1,x2-x1,y2-y1)
+ #""
+
+ if self.bounds != None: self.view.clamp_ip(self.bounds)
+
+ ox,oy = self.screen_to_tile((0,0))
+ sx,sy = self.iso_to_view((ox*iso_w,oy*iso_h))
+ dx,dy = sx - self.view.x,sy - self.view.y
+
+ for i2 in xrange(-bot,self.view.h/base_h2+bot):
+ tx,ty = ox + i2/2 + i2%2,oy + i2/2
+ x,y = (i2%2)*base_w2 + dx,i2*base_h2 + dy
+
+ #to adjust for the -1 in i1
+ x,tx,ty = x-base_w,tx-1,ty+1
+ for i1 in xrange(-1,self.view.w/base_w+2): #NOTE: not sure why +2
+ if ty >= 0 and ty < h and tx >= 0 and tx < w:
+ z = zlayer[ty][tx]*iso_z
+ if blayer != None:
+ n = blayer[ty][tx]
+ if n != 0:
+ t = tiles[n]
+ if t != None and t.image != None:
+ screen.blit(t.image,(x-base_w2,y+z))
+ n = tlayer[ty][tx]
+ if n != 0:
+ t = tiles[n]
+ if t != None and t.image != None:
+ screen.blit(t.image,(x-base_w2,y-(t.image_h-base_h)+z))
+
+ tx += 1
+ ty -= 1
+ x += base_w
+ for img,irect in todo[y/base_h2]:
+ screen.blit(img,(irect.x+adj.x,irect.y+adj.y))
+
+ return [pygame.Rect(0,0,screen.get_width(),screen.get_height())]
+
+ def iso_to_view(self,pos):
+ tlayer = self.tlayer
+ w,h = len(tlayer[0]),len(tlayer)
+
+ x,y = pos
+
+ #nx,ny = (h*self.iso_w + x - y)/2, (0 + x + y)/2
+ nx,ny = (x - y)/2, (0 + x + y)/2
+
+ return (nx * self.base_w / self.iso_w), (ny * self.base_h / self.iso_h)
+
+ def view_to_iso(self,pos):
+ tlayer = self.tlayer
+ w,h = len(tlayer[0]),len(tlayer)
+
+ x,y = pos
+
+ x,y = x*self.iso_w/self.base_w, y*self.iso_h/self.base_h
+
+ #x -= (self.iso_w/2) * h
+ #x -= (self.iso_w/2) * h
+
+ nx = (x+y)
+ ny = y*2-nx
+
+ return nx,ny
+
+ def tile_to_view(self,pos):
+ return self.iso_to_view((pos[0]*self.iso_w,pos[1]*self.iso_h))
+
+ def screen_to_tile(self,pos):
+ x,y = pos
+ x += self.view.x
+ y += self.view.y
+ x,y = self.view_to_iso((x,y))
+ return x/self.iso_w,y/self.iso_h
+
+ def tile_to_screen(self,pos):
+ x,y = self.iso_to_view((pos[0]*self.iso_w,pos[1]*self.iso_h))
+ return x-self.view.x,y-self.view.y
+
+ def tga_load_tiles(self,fname,size,tdata={}):
+ Vid.tga_load_tiles(self,fname,size,tdata)
+
+ self.tile_w,self.tile_h = size
+ self.iso_w,self.iso_h,self.iso_z = self.tile_w,self.tile_w,1
+ self.base_w,self.base_h = self.tile_w,self.tile_w/2
+
+
+
+ def resize(self,size,bg=0):
+ Vid.resize(self,size,bg)
+
+ tlayer = self.tlayer
+ w,h = len(tlayer[0]),len(tlayer)
+
+ self.zlayer = [[0 for x in xrange(0,w)] for y in xrange(0,h)]
+
+
+
+
+ def sprite_calc_irect(self,s):
+ tlayer = self.tlayer
+ w,h = len(tlayer[0]),len(tlayer)
+ zlayer = self.zlayer
+
+ x,y = self.iso_to_view((s.rect.centerx,s.rect.centery))
+ tx,ty = s.rect.centerx/self.iso_w,s.rect.centery/self.iso_h
+ z = 0
+ if ty >= 0 and ty < h and tx >= 0 and tx < w:
+ z = zlayer[ty][tx]*self.iso_z
+
+ nx,ny = x - s.shape.centerx, y - s.shape.centery + z
+
+ s.irect.x,s.irect.y = nx,ny
+
+ def run_codes(self,cdata,rect):
+ #HACK to make run_codes work
+ w,h = self.iso_w,self.iso_h
+
+ img = self.tiles[0].image
+
+ self.tiles[0].image = pygame.Surface((w,h))
+ r = Vid.run_codes(self,cdata,rect)
+ self.tiles[0].image = img
+ return r
diff --git a/pgu/layout.py b/pgu/layout.py
new file mode 100644
index 0000000..75b6c9a
--- /dev/null
+++ b/pgu/layout.py
@@ -0,0 +1,4 @@
+print 'pgu.layout','Scheduled to be deprecated.'
+
+from pgu.gui.layout import *
+
diff --git a/pgu/text.py b/pgu/text.py
new file mode 100644
index 0000000..1010a87
--- /dev/null
+++ b/pgu/text.py
@@ -0,0 +1,61 @@
+"""a collection of text rendering functions
+"""
+def write(s,font,pos,color,text,border=1):
+ """write text to a surface with a black border
+
+ <pre>write(s,font,pos,color,text,border=1)</pre>
+ """
+ i = font.render(text,1,(0,0,0))
+ si = border
+ dirs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
+ for dx,dy in dirs: s.blit(i,(pos[0]+dx*si,pos[1]+dy*si))
+ i = font.render(text,1,color)
+ s.blit(i,pos)
+
+def writec(s,font,color,text,border=1):
+ """write centered text to a surface with a black border
+
+ <pre>writec(s,font,color,text,border=1)</pre>
+ """
+ w,h = font.size(text)
+ x = (s.get_width()-w)/2
+ y = (s.get_height()-h)/2
+ write(s,font,(x,y),color,text,border)
+
+def writepre(s,font,rect,color,text):
+ """write preformatted text
+
+ <pre>writepre(s,font,rect,color,text)</pre>
+ """
+ r,c,txt = rect,color,text
+ txt = txt.replace("\t"," ")
+ i = font.render(" ",1,c)
+ sw,sh = i.get_width(),i.get_height()
+ y = r.top
+ for sentence in txt.split("\n"):
+ x = r.left
+ i = font.render(sentence,1,c)
+ s.blit(i,(x,y))
+ y += sh
+
+def writewrap(s,font,rect,color,text):
+ """write wrapped text
+
+ <pre>writewrap(s,font,rect,color,text)</pre>
+ """
+ r,c,txt = rect,color,text
+ txt = txt.replace("\t"," ")
+ i = font.render(" ",1,c)
+ sw,sh = i.get_width(),i.get_height()
+ y = r.top
+ for sentence in txt.split("\n"):
+ x = r.left
+ for word in sentence.split(" "):
+ i = font.render(word,1,c)
+ iw,ih = i.get_width(),i.get_height()
+ if x+iw > r.right: x,y = r.left,y+sh
+ s.blit(i,(x,y))
+ x += iw+sw
+ y += sh
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/tilevid.py b/pgu/tilevid.py
new file mode 100644
index 0000000..00f730d
--- /dev/null
+++ b/pgu/tilevid.py
@@ -0,0 +1,195 @@
+"""Square tile based engine."""
+
+from pgu.vid import *
+import pygame
+
+class Tilevid(Vid):
+ """Based on [[vid]] -- see for reference."""
+ def paint(self,s):
+ sw,sh = s.get_width(),s.get_height()
+ self.view.w,self.view.h = sw,sh
+
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+ w,h = self.size
+
+ if self.bounds != None: self.view.clamp_ip(self.bounds)
+
+ ox,oy = self.view.x,self.view.y
+ tlayer = self.tlayer
+ blayer = self.blayer
+ alayer = self.alayer
+ sprites = self.sprites
+
+ blit = s.blit
+ yy = - (self.view.y%th)
+ my = (oy+sh)/th
+ if (oy+sh)%th: my += 1
+
+ if blayer != None:
+ for y in xrange(oy/th,my):
+ if y >=0 and y < h:
+ trow = tlayer[y]
+ brow = blayer[y]
+ arow = alayer[y]
+ xx= - (self.view.x%tw)
+ mx = (ox+sw)/tw
+ #if (ox+sh)%tw: mx += 1
+ for x in xrange(ox/tw,mx+1):
+ if x >=0and x<w:
+ blit(tiles[brow[x]].image,(xx,yy))
+ blit(tiles[trow[x]].image,(xx,yy))
+ arow[x]=0
+ xx += tw
+ yy+=th
+ else:
+ for y in xrange(oy/th,my):
+ if y >=0 and y<h:
+ trow = tlayer[y]
+ arow = alayer[y]
+ xx= - (self.view.x%tw)
+ mx = (ox+sw)/tw
+ #if (ox+sh)%tw: mx += 1
+ for x in xrange(ox/tw,mx+1):
+ if x >=0 and x<w:
+ blit(tiles[trow[x]].image,(xx,yy))
+ arow[x]=0
+ xx += tw
+ yy+=th
+
+ for s in sprites:
+ s.irect.x = s.rect.x-s.shape.x
+ s.irect.y = s.rect.y-s.shape.y
+ blit(s.image,(s.irect.x-ox,s.irect.y-oy))
+ s.updated=0
+ s._irect = Rect(s.irect)
+ #s._rect = Rect(s.rect)
+
+ self.updates = []
+ self._view = pygame.Rect(self.view)
+ return [Rect(0,0,sw,sh)]
+
+ def update(self,s):
+ sw,sh = s.get_width(),s.get_height()
+ self.view.w,self.view.h = sw,sh
+
+ if self.bounds != None: self.view.clamp_ip(self.bounds)
+ if self.view.x != self._view.x or self.view.y != self._view.y:
+ return self.paint(s)
+
+ ox,oy = self.view.x,self.view.y
+ sw,sh = s.get_width(),s.get_height()
+ w,h = self.size
+ tlayer = self.tlayer
+ blayer = self.blayer
+ alayer = self.alayer
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+ sprites = self.sprites
+ blit = s.blit
+
+ us = []
+
+ #mark places where sprites have moved, or been removed
+
+ ss = self.sprites.removed
+ self.sprites.removed = []
+ ss.extend(sprites)
+ for s in ss:
+ #figure out what has been updated.
+ s.irect.x = s.rect.x-s.shape.x
+ s.irect.y = s.rect.y-s.shape.y
+ if (s.irect.x != s._irect.x or s.irect.y != s._irect.y
+ or s.image != s._image):
+ #w,h can be skipped, image covers that...
+ s.updated = 1
+ if s.updated:
+ r = s._irect
+ y = max(0,r.y/th)
+ yy = min(h,r.bottom/th+1)
+ while y < yy:
+ x = max(0,r.x/tw)
+ xx = min(w,r.right/tw+1)
+ while x < xx:
+ if alayer[y][x] == 0:
+ self.updates.append((x,y))
+ alayer[y][x]=1
+ x += 1
+ y += 1
+
+ r = s.irect
+ y = max(0,r.y/th)
+ yy = min(h,r.bottom/th+1)
+ while y < yy:
+ x = r.x/tw
+ xx = min(w,r.right/tw+1)
+ while x < xx:
+ if alayer[y][x]==0:
+ alayer[y][x]=2
+ self.updates.append((x,y))
+ x += 1
+ y += 1
+
+
+ #mark sprites that are not being updated that need to be updated because
+ #they are being overwritte by sprites / tiles
+ for s in sprites:
+ if s.updated==0:
+ r = s.irect
+ y = max(0,r.y/th)
+ yy = min(h,r.bottom/th+1)
+ while y < yy:
+ x = max(0,r.x/tw)
+ xx = min(w,r.right/tw+1)
+ while x < xx:
+ if alayer[y][x]==1:
+ s.updated=1
+ x += 1
+ y += 1
+
+
+ for u in self.updates:
+ x,y=u
+ xx,yy=x*tw-ox,y*th-oy
+ if alayer[y][x] == 1:
+ if blayer != None: blit(tiles[blayer[y][x]].image,(xx,yy))
+ blit(tiles[tlayer[y][x]].image,(xx,yy))
+ alayer[y][x]=0
+ us.append(Rect(xx,yy,tw,th))
+
+ for s in sprites:
+ if s.updated:
+ blit(s.image,(s.irect.x-ox, s.irect.y-oy))
+ s.updated=0
+ s._irect = Rect(s.irect)
+ s._image = s.image
+
+ self.updates = []
+ return us
+
+ def view_to_tile(self,pos):
+ x,y = pos
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+ return x/tw,y/th
+
+ def tile_to_view(self,pos):
+ x,y = pos
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+ x,y = x*tw, y*th
+ return x,y
+
+
+ def screen_to_tile(self,pos):
+ x,y = pos
+ x,y = x+self.view.x,y+self.view.y
+ return self.view_to_tile((x,y))
+
+ def tile_to_screen(self,pos):
+ x,y = pos
+ x,y = self.tile_to_view(pos)
+ x,y = x - self.view.x, y - self.view.y
+ return x,y
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/timer.py b/pgu/timer.py
new file mode 100644
index 0000000..1201b7a
--- /dev/null
+++ b/pgu/timer.py
@@ -0,0 +1,68 @@
+"""A timer for games with set-rate FPS.
+"""
+
+import pygame
+
+class Timer:
+ """A timer for games with set-rate FPS.
+
+ <pre>Timer(fps)</pre>
+ """
+
+ def __init__(self,fps):
+ if fps == 0:
+ self.tick = self._blank
+ return
+ self.wait = 1000/fps
+ self.nt = pygame.time.get_ticks()
+ pygame.time.wait(0)
+
+ def _blank(self):
+ pass
+
+ def tick(self):
+ """Wait correct amount of time each frame. Call this once per frame.
+
+ <pre>Timer.tick()</pre>
+ """
+ self.ct = pygame.time.get_ticks()
+ if self.ct < self.nt:
+ pygame.time.wait(self.nt-self.ct)
+ self.nt+=self.wait
+ else:
+ self.nt = pygame.time.get_ticks()+self.wait
+
+
+class Speedometer:
+ """A timer replacement that returns out FPS once a second.
+ <pre>Speedometer()</pre>
+
+ <strong>Attributes</strong>
+ <dl>
+ <dt>fps <dd>always set to the current FPS
+ </dl>
+ """
+ def __init__(self):
+ self.frames = 0
+ self.st = pygame.time.get_ticks()
+ self.fps = 0
+
+ def tick(self):
+ """ Call this once per frame.
+
+ <pre>Speedometer.tick()</pre>
+ """
+ r = None
+ self.frames += 1
+ self.ct = pygame.time.get_ticks()
+ if (self.ct - self.st) >= 1000:
+ r = self.fps = self.frames
+ #print "%s: %d fps"%(self.__class__.__name__,self.fps)
+ self.frames = 0
+ self.st += 1000
+ pygame.time.wait(0) #NOTE: not sure why, but you gotta call this now and again
+ return r
+
+
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/pgu/vid.py b/pgu/vid.py
new file mode 100644
index 0000000..6890e5d
--- /dev/null
+++ b/pgu/vid.py
@@ -0,0 +1,560 @@
+"""Sprite and tile engine.
+
+<p>[[tilevid]], [[isovid]], [[hexvid]] are all subclasses of
+this interface.</p>
+
+<p>Includes support for:</p>
+
+<ul>
+<li> Foreground Tiles
+<li> Background Tiles
+<li> Sprites
+<li> Sprite-Sprite Collision handling
+<li> Sprite-Tile Collision handling
+<li> Scrolling
+<li> Loading from PGU tile and sprite formats (optional)
+<li> Set rate FPS (optional)
+</ul>
+
+<p>This code was previously known as the King James Version (named after the
+Bible of the same name for historical reasons.)</p>
+"""
+
+import pygame
+from pygame.rect import Rect
+from pygame.locals import *
+import math
+
+class Sprite:
+ """The object used for Sprites.
+
+ <pre>Sprite(ishape,pos)</pre>
+
+ <dl>
+ <dt>ishape <dd>an image, or an image, rectstyle. The rectstyle will
+ describe the shape of the image, used for collision
+ detection.
+ <dt>pos <dd>initial (x,y) position of the Sprite.
+ </dl>
+
+ <strong>Attributes</strong>
+ <dl>
+ <dt>rect <dd>the current position of the Sprite
+ <dt>_rect <dd>the previous position of the Sprite
+ <dt>groups <dd>the groups the Sprite is in
+ <dt>agroups <dd>the groups the Sprite can hit in a collision
+ <dt>hit <dd>the handler for hits -- hit(g,s,a)
+ <dt>loop <dd>the loop handler, called once a frame
+ </dl>
+ """
+ def __init__(self,ishape,pos):
+ if not isinstance(ishape, tuple):
+ ishape = ishape,None
+ image,shape = ishape
+ if shape == None:
+ shape = pygame.Rect(0,0,image.get_width(),image.get_height())
+ if isinstance(shape, tuple): shape = pygame.Rect(shape)
+ self.image = image
+ self._image = self.image
+ self.shape = shape
+ self.rect = pygame.Rect(pos[0],pos[1],shape.w,shape.h)
+ self._rect = pygame.Rect(self.rect)
+ self.irect = pygame.Rect(pos[0]-self.shape.x,pos[1]-self.shape.y,
+ image.get_width(),image.get_height())
+ self._irect = pygame.Rect(self.irect)
+ self.groups = 0
+ self.agroups = 0
+ self.updated = 1
+
+ def setimage(self,ishape):
+ """Set the image of the Sprite.
+
+ <pre>Sprite.setimage(ishape)</pre>
+
+ <dl>
+ <dt>ishape <dd>an image, or an image, rectstyle. The rectstyle will
+ describe the shape of the image, used for collision detection.
+ </dl>
+ """
+ if not isinstance(ishape, tuple):
+ ishape = ishape,None
+ image,shape = ishape
+ if shape == None:
+ shape = pygame.Rect(0,0,image.get_width(),image.get_height())
+ if isinstance(shape, tuple):
+ shape = pygame.Rect(shape)
+ self.image = image
+ self.shape = shape
+ self.rect.w,self.rect.h = shape.w,shape.h
+ self.irect.w,self.irect.h = image.get_width(),image.get_height()
+ self.updated = 1
+
+
+class Tile:
+ """Tile Object used by TileCollide.
+
+ <pre>Tile(image=None)</pre>
+ <dl>
+ <dt>image <dd>an image for the Tile.
+ </dl>
+
+ <strong>Attributes</strong>
+ <dl>
+ <dt>agroups <dd>the groups the Tile can hit in a collision
+ <dt>hit <dd>the handler for hits -- hit(g,t,a)
+ </dl>
+ """
+ def __init__(self,image=None):
+ self.image = image
+ self.agroups = 0
+
+ def __setattr__(self,k,v):
+ if k == 'image' and v != None:
+ self.image_h = v.get_height()
+ self.image_w = v.get_width()
+ self.__dict__[k] = v
+
+class _Sprites(list):
+ def __init__(self):
+ list.__init__(self)
+ self.removed = []
+
+ def append(self,v):
+ list.append(self,v)
+ v.updated = 1
+
+ def remove(self,v):
+ list.remove(self,v)
+ v.updated = 1
+ self.removed.append(v)
+
+class Vid:
+ """An engine for rendering Sprites and Tiles.
+
+ <pre>Vid()</pre>
+
+ <strong>Attributes</strong>
+ <dl>
+ <dt>sprites <dd>a list of the Sprites to be displayed. You may append and
+ remove Sprites from it.
+ <dt>images <dd>a dict for images to be put in.
+ <dt>size <dd>the width, height in Tiles of the layers. Do not modify.
+ <dt>view <dd>a pygame.Rect of the viewed area. You may change .x, .y,
+ etc to move the viewed area around.
+ <dt>bounds <dd>a pygame.Rect (set to None by default) that sets the bounds
+ of the viewable area. Useful for setting certain borders
+ as not viewable.
+ <dt>tlayer <dd>the foreground tiles layer
+ <dt>clayer <dd>the code layer (optional)
+ <dt>blayer <dd>the background tiles layer (optional)
+ <dt>groups <dd>a hash of group names to group values (32 groups max, as a tile/sprites
+ membership in a group is determined by the bits in an integer)
+ </dl>
+ """
+
+ def __init__(self):
+ self.tiles = [None for x in xrange(0,256)]
+ self.sprites = _Sprites()
+ self.images = {} #just a store for images.
+ self.layers = None
+ self.size = None
+ self.view = pygame.Rect(0,0,0,0)
+ self._view = pygame.Rect(self.view)
+ self.bounds = None
+ self.updates = []
+ self.groups = {}
+
+
+ def resize(self,size,bg=0):
+ """Resize the layers.
+
+ <pre>Vid.resize(size,bg=0)</pre>
+
+ <dl>
+ <dt>size <dd>w,h in Tiles of the layers
+ <dt>bg <dd>set to 1 if you wish to use both a foreground layer and a
+ background layer
+ </dl>
+ """
+ self.size = size
+ w,h = size
+ self.layers = [[[0 for x in xrange(0,w)] for y in xrange(0,h)]
+ for z in xrange(0,4)]
+ self.tlayer = self.layers[0]
+ self.blayer = self.layers[1]
+ if not bg: self.blayer = None
+ self.clayer = self.layers[2]
+ self.alayer = self.layers[3]
+
+ self.view.x, self.view.y = 0,0
+ self._view.x, self.view.y = 0,0
+ self.bounds = None
+
+ self.updates = []
+
+ def set(self,pos,v):
+ """Set a tile in the foreground to a value.
+
+ <p>Use this method to set tiles in the foreground, as it will make
+ sure the screen is updated with the change. Directly changing
+ the tlayer will not guarantee updates unless you are using .paint()
+ </p>
+
+ <pre>Vid.set(pos,v)</pre>
+
+ <dl>
+ <dt>pos <dd>(x,y) of tile
+ <dt>v <dd>value
+ </dl>
+ """
+ if self.tlayer[pos[1]][pos[0]] == v: return
+ self.tlayer[pos[1]][pos[0]] = v
+ self.alayer[pos[1]][pos[0]] = 1
+ self.updates.append(pos)
+
+ def get(self,pos):
+ """Get the tlayer at pos.
+
+ <pre>Vid.get(pos): return value</pre>
+
+ <dl>
+ <dt>pos <dd>(x,y) of tile
+ </dl>
+ """
+ return self.tlayer[pos[1]][pos[0]]
+
+ def paint(self,s):
+ """Paint the screen.
+
+ <pre>Vid.paint(screen): return [updates]</pre>
+
+ <dl>
+ <dt>screen <dd>a pygame.Surface to paint to
+ </dl>
+
+ <p>returns the updated portion of the screen (all of it)</p>
+ """
+ return []
+
+ def update(self,s):
+ """Update the screen.
+
+ <pre>Vid.update(screen): return [updates]</pre>
+
+ <dl>
+ <dt>screen <dd>a pygame.Rect to update
+ </dl>
+
+ <p>returns a list of updated rectangles.</p>
+ """
+ self.updates = []
+ return []
+
+ def tga_load_level(self,fname,bg=0):
+ """Load a TGA level.
+
+ <pre>Vid.tga_load_level(fname,bg=0)</pre>
+
+ <dl>
+ <dt>g <dd>a Tilevid instance
+ <dt>fname <dd>tga image to load
+ <dt>bg <dd>set to 1 if you wish to load the background layer
+ </dl>
+ """
+ if type(fname) == str: img = pygame.image.load(fname)
+ else: img = fname
+ w,h = img.get_width(),img.get_height()
+ self.resize((w,h),bg)
+ for y in range(0,h):
+ for x in range(0,w):
+ t,b,c,_a = img.get_at((x,y))
+ self.tlayer[y][x] = t
+ if bg: self.blayer[y][x] = b
+ self.clayer[y][x] = c
+
+ def tga_save_level(self,fname):
+ """Save a TGA level.
+
+ <pre>Vid.tga_save_level(fname)</pre>
+
+ <dl>
+ <dt>fname <dd>tga image to save to
+ </dl>
+ """
+ w,h = self.size
+ img = pygame.Surface((w,h),SWSURFACE,32)
+ img.fill((0,0,0,0))
+ for y in range(0,h):
+ for x in range(0,w):
+ t = self.tlayer[y][x]
+ b = 0
+ if self.blayer:
+ b = self.blayer[y][x]
+ c = self.clayer[y][x]
+ _a = 0
+ img.set_at((x,y),(t,b,c,_a))
+ pygame.image.save(img,fname)
+
+
+
+ def tga_load_tiles(self,fname,size,tdata={}):
+ """Load a TGA tileset.
+
+ <pre>Vid.tga_load_tiles(fname,size,tdata={})</pre>
+
+ <dl>
+ <dt>g <dd>a Tilevid instance
+ <dt>fname <dd>tga image to load
+ <dt>size <dd>(w,h) size of tiles in pixels
+ <dt>tdata <dd>tile data, a dict of tile:(agroups, hit handler, config)
+ </dl>
+ """
+ TW,TH = size
+ if type(fname) == str: img = pygame.image.load(fname).convert_alpha()
+ else: img = fname
+ w,h = img.get_width(),img.get_height()
+
+ n = 0
+ for y in range(0,h,TH):
+ for x in range(0,w,TW):
+ i = img.subsurface((x,y,TW,TH))
+ tile = Tile(i)
+ self.tiles[n] = tile
+ if n in tdata:
+ agroups,hit,config = tdata[n]
+ tile.agroups = self.string2groups(agroups)
+ tile.hit = hit
+ tile.config = config
+ n += 1
+
+
+ def load_images(self,idata):
+ """Load images.
+
+ <pre>Vid.load_images(idata)</pre>
+
+ <dl>
+ <dt>idata <dd>a list of (name, fname, shape)
+ </dl>
+ """
+ for name,fname,shape in idata:
+ self.images[name] = pygame.image.load(fname).convert_alpha(),shape
+
+ def run_codes(self,cdata,rect):
+ """Run codes.
+
+ <pre>Vid.run_codes(cdata,rect)</pre>
+
+ <dl>
+ <dt>cdata <dd>a dict of code:(handler function, value)
+ <dt>rect <dd>a tile rect of the parts of the layer that should have
+ their codes run
+ </dl>
+ """
+ tw,th = self.tiles[0].image.get_width(),self.tiles[0].image.get_height()
+
+ x1,y1,w,h = rect
+ clayer = self.clayer
+ t = Tile()
+ for y in range(y1,y1+h):
+ for x in range(x1,x1+w):
+ n = clayer[y][x]
+ if n in cdata:
+ fnc,value = cdata[n]
+ t.tx,t.ty = x,y
+ t.rect = pygame.Rect(x*tw,y*th,tw,th)
+ fnc(self,t,value)
+
+
+ def string2groups(self,str):
+ """Convert a string to groups.
+
+ <pre>Vid.string2groups(str): return groups</pre>
+ """
+ if str == None: return 0
+ return self.list2groups(str.split(","))
+
+ def list2groups(self,igroups):
+ """Convert a list to groups.
+ <pre>Vid.list2groups(igroups): return groups</pre>
+ """
+ for s in igroups:
+ if not s in self.groups:
+ self.groups[s] = 2**len(self.groups)
+ v = 0
+ for s,n in self.groups.items():
+ if s in igroups: v|=n
+ return v
+
+ def groups2list(self,groups):
+ """Convert a groups to a list.
+ <pre>Vid.groups2list(groups): return list</pre>
+ """
+ v = []
+ for s,n in self.groups.items():
+ if (n&groups)!=0: v.append(s)
+ return v
+
+ def hit(self,x,y,t,s):
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+ t.tx = x
+ t.ty = y
+ t.rect = Rect(x*tw,y*th,tw,th)
+ t._rect = t.rect
+ if hasattr(t,'hit'):
+ t.hit(self,t,s)
+
+ def loop(self):
+ """Update and hit testing loop. Run this once per frame.
+ <pre>Vid.loop()</pre>
+ """
+ self.loop_sprites() #sprites may move
+ self.loop_tilehits() #sprites move
+ self.loop_spritehits() #no sprites should move
+ for s in self.sprites:
+ s._rect = pygame.Rect(s.rect)
+
+ def loop_sprites(self):
+ as_ = self.sprites[:]
+ for s in as_:
+ if hasattr(s,'loop'):
+ s.loop(self,s)
+
+ def loop_tilehits(self):
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+
+ layer = self.layers[0]
+
+ as_ = self.sprites[:]
+ for s in as_:
+ self._tilehits(s)
+
+ def _tilehits(self,s):
+ tiles = self.tiles
+ tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
+ layer = self.layers[0]
+
+ for _z in (0,):
+ if s.groups != 0:
+
+ _rect = s._rect
+ rect = s.rect
+
+ _rectx = _rect.x
+ _recty = _rect.y
+ _rectw = _rect.w
+ _recth = _rect.h
+
+ rectx = rect.x
+ recty = rect.y
+ rectw = rect.w
+ recth = rect.h
+
+ rect.y = _rect.y
+ rect.h = _rect.h
+
+ hits = []
+ ct,cb,cl,cr = rect.top,rect.bottom,rect.left,rect.right
+ #nasty ol loops
+ y = ct/th*th
+ while y < cb:
+ x = cl/tw*tw
+ yy = y/th
+ while x < cr:
+ xx = x/tw
+ t = tiles[layer[yy][xx]]
+ if (s.groups & t.agroups)!=0:
+ #self.hit(xx,yy,t,s)
+ d = math.hypot(rect.centerx-(xx*tw+tw/2),
+ rect.centery-(yy*th+th/2))
+ hits.append((d,t,xx,yy))
+
+ x += tw
+ y += th
+
+ hits.sort()
+ #if len(hits) > 0: print self.frame,hits
+ for d,t,xx,yy in hits:
+ self.hit(xx,yy,t,s)
+
+ #switching directions...
+ _rect.x = rect.x
+ _rect.w = rect.w
+ rect.y = recty
+ rect.h = recth
+
+ hits = []
+ ct,cb,cl,cr = rect.top,rect.bottom,rect.left,rect.right
+ #nasty ol loops
+ y = ct/th*th
+ while y < cb:
+ x = cl/tw*tw
+ yy = y/th
+ while x < cr:
+ xx = x/tw
+ t = tiles[layer[yy][xx]]
+ if (s.groups & t.agroups)!=0:
+ d = math.hypot(rect.centerx-(xx*tw+tw/2),
+ rect.centery-(yy*th+th/2))
+ hits.append((d,t,xx,yy))
+ #self.hit(xx,yy,t,s)
+ x += tw
+ y += th
+
+ hits.sort()
+ #if len(hits) > 0: print self.frame,hits
+ for d,t,xx,yy in hits:
+ self.hit(xx,yy,t,s)
+
+ #done with loops
+ _rect.x = _rectx
+ _rect.y = _recty
+
+
+ def loop_spritehits(self):
+ as_ = self.sprites[:]
+
+ groups = {}
+ for n in range(0,31):
+ groups[1<<n] = []
+ for s in as_:
+ g = s.groups
+ n = 1
+ while g:
+ if (g&1)!=0: groups[n].append(s)
+ g >>= 1
+ n <<= 1
+
+ for s in as_:
+ if s.agroups!=0:
+ rect1,rect2 = s.rect,Rect(s.rect)
+ #if rect1.centerx < 320: rect2.x += 640
+ #else: rect2.x -= 640
+ g = s.agroups
+ n = 1
+ while g:
+ if (g&1)!=0:
+ for b in groups[n]:
+ if (s != b and (s.agroups & b.groups)!=0
+ and s.rect.colliderect(b.rect)):
+ s.hit(self,s,b)
+
+ g >>= 1
+ n <<= 1
+
+
+ def screen_to_tile(self,pos):
+ """Convert a screen position to a tile position.
+ <pre>Vid.screen_to_tile(pos): return pos</pre>
+ """
+ return pos
+
+ def tile_to_screen(self,pos):
+ """Convert a tile position to a screen position.
+ <pre>Vid.tile_to_screen(pos): return pos</pre>
+ """
+ return pos
+
+# vim: set filetype=python sts=4 sw=4 noet si :
diff --git a/po/ConozcoNumeros.pot b/po/ConozcoNumeros.pot
new file mode 100644
index 0000000..5e0c38f
--- /dev/null
+++ b/po/ConozcoNumeros.pot
@@ -0,0 +1,58 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-04-16 11:36-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: activity/activity.info:2
+msgid "Conozco Numeros"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/activity.py:8
+msgid "Conozco Mexico"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:265
+msgid "Acerca de Conozco Mexico"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:283
+msgid "Presiona cualquier tecla para volver"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:303
+msgid "CONOZCO MEXICO"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:308
+msgid "Juego"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:328
+msgid "Sobre este juego"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:336
+msgid "Salir"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/conozcomx.py:546
+msgid "Terminar"
+msgstr ""
+
+#: /home/sin/olpc/conozco-mexico/mainline/olpcgames/canvas.py:151
+#, python-format
+msgid "%s Source"
+msgstr ""
diff --git a/recursos/comun/datos/creditos.txt b/recursos/comun/datos/creditos.txt
index f47a5d8..3653bce 100755
--- a/recursos/comun/datos/creditos.txt
+++ b/recursos/comun/datos/creditos.txt
@@ -1,9 +1,9 @@
Autor: Cole
+Con la ayuda y consejo del Profesor Jose Icaza, Tec de Monterrey, Unidad Monterrey
Este programa es software libre y desarrollado por la comunidad
Este programa está basado en el codigo de http://ceibaljam.org
Mapa de Mexico, original: Alex Covarrubias (Wikimedia Commons)
-Gráficos de las casitas: Federico Mandian
-Sonidos bajados de freesound.org: btn045.wav gentileza de junggle
-Fuentes bajadas de urbanfonts.com: AllCaps.ttf gentileza de BobFrantic y Share-Regular.ttf gentileza de anónimo
+Tema de la interfax grafical: Story-Builder
+El codigo es basado en StoryBuilder y Conozco / Conozco-Numeros
diff --git a/recursos/comun/datos/creditos.txt~ b/recursos/comun/datos/creditos.txt~
index efce538..f47a5d8 100644
--- a/recursos/comun/datos/creditos.txt~
+++ b/recursos/comun/datos/creditos.txt~
@@ -3,6 +3,7 @@ Autor: Cole
Este programa es software libre y desarrollado por la comunidad
Este programa está basado en el codigo de http://ceibaljam.org
+Mapa de Mexico, original: Alex Covarrubias (Wikimedia Commons)
Gráficos de las casitas: Federico Mandian
Sonidos bajados de freesound.org: btn045.wav gentileza de junggle
Fuentes bajadas de urbanfonts.com: AllCaps.ttf gentileza de BobFrantic y Share-Regular.ttf gentileza de anónimo
diff --git a/recursos/lamina/LICENSE b/recursos/lamina/LICENSE
new file mode 100644
index 0000000..2c92af3
--- /dev/null
+++ b/recursos/lamina/LICENSE
@@ -0,0 +1,10 @@
+
+Las imágenes en este directorio están licenciadas bajo una Licencia
+Atribución-No Comercial-Compartir Obras Derivadas Igual 3.0 Unported
+de Creative Commons. Para ver una copia de esta licencia, visite
+http://creativecommons.org/licenses/by-nc-sa/3.0/ o envíenos una carta
+a Creative Commons, 171 Second Street, Suite 300, San Francisco,
+California, 94105, USA.
+
+Todas las imágenes fueron creadas por Gabriel Eirea, menos terron.png
+que fue creada por Alejandro Rodríguez Juele.
diff --git a/recursos/lamina/bicho.png b/recursos/lamina/bicho.png
new file mode 100755
index 0000000..c13af86
--- /dev/null
+++ b/recursos/lamina/bicho.png
Binary files differ
diff --git a/recursos/lamina/capital_icon.png b/recursos/lamina/capital_icon.png
new file mode 100644
index 0000000..220f9bd
--- /dev/null
+++ b/recursos/lamina/capital_icon.png
Binary files differ
diff --git a/recursos/lamina/estado_icon.xcf b/recursos/lamina/estado_icon.xcf
new file mode 100644
index 0000000..ee260c5
--- /dev/null
+++ b/recursos/lamina/estado_icon.xcf
Binary files differ
diff --git a/recursos/lamina/globito.png b/recursos/lamina/globito.png
new file mode 100755
index 0000000..23eec7c
--- /dev/null
+++ b/recursos/lamina/globito.png
Binary files differ
diff --git a/recursos/lamina/layout.png b/recursos/lamina/layout.png
new file mode 100644
index 0000000..f7e769a
--- /dev/null
+++ b/recursos/lamina/layout.png
Binary files differ
diff --git a/recursos/lamina/layout.png.orig b/recursos/lamina/layout.png.orig
new file mode 100644
index 0000000..59d781e
--- /dev/null
+++ b/recursos/lamina/layout.png.orig
Binary files differ
diff --git a/recursos/lamina/layout_cmx.png b/recursos/lamina/layout_cmx.png
new file mode 100644
index 0000000..ab24332
--- /dev/null
+++ b/recursos/lamina/layout_cmx.png
Binary files differ
diff --git a/recursos/lamina/levels_capitals.txt b/recursos/lamina/levels_capitals.txt
new file mode 100644
index 0000000..c81323c
--- /dev/null
+++ b/recursos/lamina/levels_capitals.txt
@@ -0,0 +1,59 @@
+Prefijo = Tenemos que buscar una pieza de mi nave en\
+Prefijo = Hay una pieza de mi nave en\
+Prefijo = Según mi información tenemos que buscar en\
+Prefijo = Tenemos que ir hasta\
+Prefijo = Una pieza de mi cohete cayó en\
+
+Sufijo = ¿Podés señalar dónde es?
+Sufijo = ¿Dónde queda?
+Sufijo = ¿Vamos hasta ahí?
+Sufijo = ¿Me llevás?
+Sufijo = ¿Podés mostrarme dónde es?
+
+Correcto = ¡Muy bien!
+Correcto = ¡Genial!
+Correcto = ¡Lo encontraste!
+Correcto = ¡Sí!
+
+Mal = No, intentá de nuevo
+Mal = Acá no está
+Mal = Parece que no es acá
+Mal = Seguí probando...
+
+Despedida = Ahora puedo volver a mi planeta. ¡Gracias por tu ayuda!
+Despedida = Mi nave está pronta. ¡Chau y gracias por ayudarme!
+Despedida = ¡Lo hicimos! Estoy listo para despegar. ¡Hasta la próxima!
+
+[Nivel 1]
+Pregunta = Mexicali|1|
+Pregunta = La Paz|2|
+Pregunta = Hermosillo|3|
+Pregunta = Chihuahua|4|
+Pregunta = Saltillo|5|
+Pregunta = Culiacan|6|
+Pregunta = Durango|7|
+Pregunta = Monterrey|8|
+Pregunta = Ciudad Victoria|9|
+Pregunta = Zacatecas|10|
+Pregunta = Tepic|11|
+Pregunta = San Luis Potosí|12|
+Pregunta = Aguascalientes|13|
+Pregunta = Guadalajara|14|
+Pregunta = Guanajuato|15|
+Pregunta = Querétaro|16|
+Pregunta = Jalapa|17|
+Pregunta = Colima|18|
+Pregunta = Pachuca|19|
+Pregunta = Morelia|20|
+Pregunta = Toluca|21|
+Pregunta = Chilpancingo|22|
+Pregunta = Cuernavaca|23|
+Pregunta = Tlaxcala|24|
+Pregunta = Puebla|25|
+Pregunta = Oaxaca|26|
+Pregunta = Mexico|27|
+Pregunta = Villahermosa|28|
+Pregunta = Tuxtla Gutierrez|29|
+Pregunta = Campeche|30|
+Pregunta = Merida|31|
+Pregunta = Chetumal|32|
diff --git a/recursos/lamina/levels_states.txt b/recursos/lamina/levels_states.txt
new file mode 100644
index 0000000..289ae23
--- /dev/null
+++ b/recursos/lamina/levels_states.txt
@@ -0,0 +1,59 @@
+Prefijo = Tenemos que buscar una pieza de mi nave en\
+Prefijo = Hay una pieza de mi nave en\
+Prefijo = Según mi información tenemos que buscar en\
+Prefijo = Tenemos que ir hasta\
+Prefijo = Una pieza de mi cohete cayó en\
+
+Sufijo = ¿Podés señalar dónde es?
+Sufijo = ¿Dónde queda?
+Sufijo = ¿Vamos hasta ahí?
+Sufijo = ¿Me llevás?
+Sufijo = ¿Podés mostrarme dónde es?
+
+Correcto = ¡Muy bien!
+Correcto = ¡Genial!
+Correcto = ¡Lo encontraste!
+Correcto = ¡Sí!
+
+Mal = No, intentá de nuevo
+Mal = Acá no está
+Mal = Parece que no es acá
+Mal = Seguí probando...
+
+Despedida = Ahora puedo volver a mi planeta. ¡Gracias por tu ayuda!
+Despedida = Mi nave está pronta. ¡Chau y gracias por ayudarme!
+Despedida = ¡Lo hicimos! Estoy listo para despegar. ¡Hasta la próxima!
+
+[Nivel 1]
+Pregunta = el estado de Baja California|1|
+Pregunta = el estado de Baja California Sur|2|
+Pregunta = el estado de Sonora|3|
+Pregunta = el estado de Chihuahua|4|
+Pregunta = el estado de Coahuila|5|
+Pregunta = el estado de Sinaloa|6|
+Pregunta = el estado de Durango|7|
+Pregunta = el estado de Nuevo Leon|8|
+Pregunta = el estado de Tamaulipas|9|
+Pregunta = el estado de Zacatecas|10|
+Pregunta = el estado de Nayarit|11|
+Pregunta = el estado de San Luis Potosí|12|
+Pregunta = el estado de Aguascalientes|13|
+Pregunta = el estado de Jalisco|14|
+Pregunta = el estado de Guanajuato|15|
+Pregunta = el estado de Querétaro|16|
+Pregunta = el estado de Veracruz|17|
+Pregunta = el estado de Colima|18|
+Pregunta = el estado de Hidalgo|19|
+Pregunta = el estado de Michoacán|20|
+Pregunta = el Estado de México|21|
+Pregunta = el estado de Guerrero|22|
+Pregunta = el estado de Morelos|23|
+Pregunta = el estado de Tlaxcala|24|
+Pregunta = el estado de Puebla|25|
+Pregunta = el estado de Oaxaca|26|
+Pregunta = el Distrito Federal|27|
+Pregunta = el estado de Tabasco|28|
+Pregunta = el estado de Chiapas|29|
+Pregunta = el estado de Campeche|30|
+Pregunta = el estado de Yucatán|31|
+Pregunta = el estado de Quintana Roo|32|
diff --git a/recursos/lamina/mexico.png b/recursos/lamina/mexico.png
new file mode 100644
index 0000000..4347724
--- /dev/null
+++ b/recursos/lamina/mexico.png
Binary files differ
diff --git a/recursos/lamina/mexico_capitals_long.png b/recursos/lamina/mexico_capitals_long.png
new file mode 100644
index 0000000..098db8c
--- /dev/null
+++ b/recursos/lamina/mexico_capitals_long.png
Binary files differ
diff --git a/recursos/lamina/mexico_estados.xcf b/recursos/lamina/mexico_estados.xcf
index 2764561..cfdbed5 100644
--- a/recursos/lamina/mexico_estados.xcf
+++ b/recursos/lamina/mexico_estados.xcf
Binary files differ
diff --git a/recursos/lamina/mexico_estados_long.xcf b/recursos/lamina/mexico_estados_long.xcf
new file mode 100644
index 0000000..bcccac4
--- /dev/null
+++ b/recursos/lamina/mexico_estados_long.xcf
Binary files differ
diff --git a/recursos/lamina/mexico_estados_scaled.png b/recursos/lamina/mexico_estados_scaled.png
new file mode 100644
index 0000000..41abb1c
--- /dev/null
+++ b/recursos/lamina/mexico_estados_scaled.png
Binary files differ
diff --git a/recursos/lamina/mexico_estados_scaled.xcf b/recursos/lamina/mexico_estados_scaled.xcf
new file mode 100644
index 0000000..2c61310
--- /dev/null
+++ b/recursos/lamina/mexico_estados_scaled.xcf
Binary files differ
diff --git a/recursos/lamina/mexico_estados_solo.png b/recursos/lamina/mexico_estados_solo.png
index 43de251..1f9578c 100644
--- a/recursos/lamina/mexico_estados_solo.png
+++ b/recursos/lamina/mexico_estados_solo.png
Binary files differ
diff --git a/recursos/lamina/mexico_states_long.png b/recursos/lamina/mexico_states_long.png
new file mode 100644
index 0000000..013bbb9
--- /dev/null
+++ b/recursos/lamina/mexico_states_long.png
Binary files differ
diff --git a/recursos/lamina/niveles.txt b/recursos/lamina/niveles.txt.backup
index c5d8831..c5d8831 100644
--- a/recursos/lamina/niveles.txt
+++ b/recursos/lamina/niveles.txt.backup
diff --git a/recursos/lamina/niveles.txt~ b/recursos/lamina/niveles.txt~
new file mode 100644
index 0000000..640414a
--- /dev/null
+++ b/recursos/lamina/niveles.txt~
@@ -0,0 +1,181 @@
+Prefijo = Tenemos que buscar una pieza de mi nave en\
+Prefijo = Hay una pieza de mi nave en\
+Prefijo = Según mi información tenemos que buscar en\
+Prefijo = Tenemos que ir hasta\
+Prefijo = Una pieza de mi cohete cayó en\
+
+Sufijo = ¿Podés señalar dónde es?
+Sufijo = ¿Dónde queda?
+Sufijo = ¿Vamos hasta ahí?
+Sufijo = ¿Me llevás?
+Sufijo = ¿Podés mostrarme dónde es?
+
+Correcto = ¡Muy bien!
+Correcto = ¡Genial!
+Correcto = ¡Lo encontraste!
+Correcto = ¡Sí!
+
+Mal = No, intentá de nuevo
+Mal = Acá no está
+Mal = Parece que no es acá
+Mal = Seguí probando...
+
+Despedida = Ahora puedo volver a mi planeta. ¡Gracias por tu ayuda!
+Despedida = Mi nave está pronta. ¡Chau y gracias por ayudarme!
+Despedida = ¡Lo hicimos! Estoy listo para despegar. ¡Hasta la próxima!
+
+[Nivel 1]
+Pregunta = la casa del mayor divisor común de 48 y 49|1|Es el mas chico de todos
+Pregunta = la casa del mayor divisor común de 96 y 98|2|Es el primero con un zapato
+Pregunta = la casa del mayor divisor común de 96 y 99|3|Es el segundo con un zapato
+Pregunta = la casa del mayor divisor común de 96 y 92|4|Es un edificio azul\sobre la primer fila
+Pregunta = la casa del mayor divisor común de 95 y 90|5|Es el tercero con un zapato
+Pregunta = la casa del mayor divisor común de 96 y 90|6|Es verde, está en la\la primer fila y tiene ventanas amarillas
+Pregunta = la casa del mayor divisor común de 91 y 98|7|Es un triángulo y\está junto a la casa Jarrón
+Pregunta = la casa del mayor divisor común de 96 y 88|8|Es un jarrón y está en\la primer fila
+Pregunta = la casa del mayor divisor común de 99 y 91|9|Visto al revés se lee seis
+Pregunta = la casa del mayor divisor común de 90 y 100|10|El primero arriba\a la izquierda
+Pregunta = la casa del mayor divisor común de 88 y 99 |11|El primer zapato\de la segunda fila
+Pregunta = la casa del mayor divisor común de 84 y 96 |12|Una casa naraja al\lado de la tetera
+Pregunta = la casa del mayor divisor común de 91 y 78 |13|Es la tetera que tiene\el sexto zapato
+Pregunta = la casa del mayor divisor común de 98 y 84|14|Parece una fábrica\está en la segunda fila
+Pregunta = la casa del mayor divisor común de 75 y 90 |15|Termina en 5, no es\una TV y es amarilla
+Pregunta = la casa del mayor divisor común de 32 y 48|16|Segunda fila al medio\y en la tabla del 4
+Pregunta = la casa del mayor divisor común de 34 y 51|17|Segundo zapato de\la segunda fila
+Pregunta = la casa del mayor divisor común de 36 y 54|18|Es grande violeta con\ventanas rojas
+Pregunta = la casa del mayor divisor común de 38 y 57|19|Verde alta y con zapato
+Pregunta = la casa del mayor divisor común de 40 y 60|20|Es una torre inclinada
+Pregunta = la casa del mayor divisor común de 42 y 63|21|Tiene gran antena de TV
+Pregunta = la casa del mayor divisor común de 44 y 66|22|Es la casa con flor
+Pregunta = la casa del mayor divisor común de 46 y 69|23|Es la casa latita
+Pregunta = la casa del mayor divisor común de 48 y 72|24|Es verde con techo\celeste y puerta gris
+Pregunta = la casa del mayor divisor común de 50 y 75|25|Está en la tabla del 5\y junto a la del techo que parece un embudo
+Pregunta = la casa del mayor divisor común de 52 y 78|26|Es la que tiene el techo\como un embudo de la tercer fila
+Pregunta = la casa del mayor divisor común de 54 y 81|27|Tiene techo amarillo\y no es la 77
+Pregunta = la casa del mayor divisor común de 56 y 84|28|Tiene el número en el\árbol y es par
+Pregunta = la casa del mayor divisor común de 58 y 87|29|Tiene zapato tipo\champión en la puerta
+Pregunta = la casa del mayor divisor común de 60 y 90|30|Está bajo la torre inclinada\y en tabla del 3
+Pregunta = la casa del mayor divisor común de 62 y 93|31|Es rosada y tiene\zapato
+Pregunta = la casa del mayor divisor común de 64 y 96|32|Es verde y pequeña
+Pregunta = la casa del mayor divisor común de 66 y 99|33|Tiene techo amarillo\y es roja
+Pregunta = la casa del mayor divisor común de 68 y 34|34|Totalmente celeste y\no es tetera
+Pregunta = la casa del mayor divisor común de 70 y 35|35|Tiene 3 árboles a la\izquierda y está en la tabla del 5
+Pregunta = la casa del mayor divisor común de 72 y 36|36|Es celeste y está\junto al helicóptero
+Pregunta = la casa del mayor divisor común de 74 y 37|37|Es un helicóptero con zapato
+Pregunta = la casa del mayor divisor común de 76 y 38|38|Es una casa grande\ y amarilla
+Pregunta = la casa del mayor divisor común de 78 y 39|39|Está junto a la manzana\ y es menor que 50
+Pregunta = la casa del mayor divisor común de 80 y 40|40|No es una naranja\pero es una fruta
+Pregunta = la casa del mayor divisor común de 82 y 41|41|Tiene el zapato de\los primos y está junto al cohete
+Pregunta = la casa del mayor divisor común de 84 y 42|42|Es un planeta con el\cohete en la puerta
+Pregunta = la casa del mayor divisor común de 86 y 43|43|Tiene zapato rojo de\la quinta fila
+Pregunta = la casa del mayor divisor común de 88 y 44|44|Es la rosada de\ la tabla del 11
+Pregunta = la casa del mayor divisor común de 90 y 45|45|Es la única TV
+Pregunta = la casa del mayor divisor común de 92 y 46|46|Está junto a la TV y\no es rosada
+Pregunta = la casa del mayor divisor común de 94 y 47|47|Está dentro de una valija
+Pregunta = la casa del mayor divisor común de 96 y 48|48|No es una manzana\y tiene un auto
+Pregunta = la casa del mayor divisor común de 98 y 49|49|Es la casa que es\un asteroide
+Pregunta = la casa del mayor divisor común de 100 y 50|50|Tiene el techo de embudo
+Pregunta = la casa del primer múltiplo de 17 y de 3|51|Tiene el único zapato\rojo en la primer columna
+Pregunta = la casa del primer múltiplo de 13 y de 4|52|Es gris y está entre\primos gemelos
+Pregunta = la casa del número que es la mitad de 106|53|Está arriba de una tetera\pero no al lado de otra con zapato
+Pregunta = la casa del múltiplo de 9 que empieza con 5|54|Es igual a la casa del 4
+Pregunta = la casa del primer múltiplo de 5 y de 11|55|Es como la 5 pero sin\champión
+Pregunta = la casa del primer múltiplo de 7 y de 8 |56|Es como la casa 6 pero\mucho mas grande
+Pregunta = la casa del primer múltiplo de 19 y de 3|57|Es un triángulo y\está junto a la casa Jarrón
+Pregunta = la casa del primer múltiplo de 29 y de 2|58|Es fácil, es un jarrón
+Pregunta = la casa del vecino de 60 que es primo y menor que él|59|Está junto al jarrón\y tiene un zapato
+Pregunta = la casa del primer múltiplo de 10 y de 6|60|Es como la casa 10\pero mayor que 50
+Pregunta = la casa del vecino de 60 que es primo y mayor que él|61|Es negra, es impar\y no es el 11
+Pregunta = la casa del múltiplo de 31 que es par|62|Está entre una negra y\la tetera
+Pregunta = la casa del múltiplo de 9 que empieza con 6|63|Es tetera y no tiene\zapato
+Pregunta = la casa de la mayor potencia de 2|64|Parece una fábrica\y no es la 14
+Pregunta = la casa del primer múltiplo de 13 y de 5|65|Termina en 5, no es\una TV y es amarilla
+Pregunta = la casa del primer múltiplo de 6 y de 11|66|Si la veo al revés es 99
+Pregunta = la casa de la tercera parte de 201|67|Tiene un zapato\y una estrella
+Pregunta = la casa del primer múltiplo de 17 y de 4|68|Es grande violeta con\ventanas rojas
+Pregunta = la casa del primer múltiplo de 23 y de 3|69|Verde alta pero\sin zapato
+Pregunta = la casa del múltiplo de 10 y de 7|70|Es una torre inclinada
+Pregunta = la casa de la tercera parte de 213|71|Tiene zapato y está\junto a una flor
+Pregunta = la casa del múltiplo de 9 que empieza con 3|72|Es la casa con flor
+Pregunta = la casa de la mitad de 146|73|Es la casa latita
+Pregunta = la casa del doble del primo número 12|74|Es verde con techo\celeste y puerta gris
+Pregunta = la casa del múltiplo de 25 que no es 25 y es impar|75|Está en la tabla del 25\y junto a la del techo que parece un embudo
+Pregunta = la casa del múltiplo de 19 y de 4|76|Es la que tiene el techo\como un embudo
+Pregunta = la casa del múltiplo de 7 y de 11|77|Tiene techo amarillo\y no es la 27
+Pregunta = la casa del que está en la tabla del 13\y es múltiplo de 6|78|Tiene el número en el árbol y es par
+Pregunta = la casa del impar que está al lado de 80 pero no es\potencia de tres|79|Tiene zapato tipo champión en la puerta
+Pregunta = la casa del múltiplo de 20 que también es múltiplo de 8|80|Está bajo la torre\inclinada y en tabla del 10
+Pregunta = la casa de la mas alta potencia de tres|81|Es rosada y no tiene\zapato
+Pregunta = la casa de mayor múltiplo de 41|82|Es verde y pequeña
+Pregunta = la casa del primo que pasó los 80|83|Tiene zapato, techo\amarillo y es roja
+Pregunta = la casa del mayor múltiplo de 7 y de 6 |84|Totalmente celeste y\no es tetera
+Pregunta = la casa del mayor múltiplo de 17|85|Tiene 3 árboles a la \izquierda y está\en la tabla del 5
+Pregunta = la casa del múltiplo de 43 que es par|86|Es celeste y está\junto al helicóptero
+Pregunta = la casa del mayor múltiplo de 29|87|Es un helicóptero con zapato
+Pregunta = la casa del múltiplo de 8 y de 11|88|Es una casa grande\y amarilla
+Pregunta = la casa del que es primo y al revés se lee 68|89|Está junto a la manzana\y es mayor que 50
+Pregunta = la casa del número que entre 81 y 99 es múltiplo de 10|90|No es una naranja\pero es una fruta
+Pregunta = la casa del mayor múltiplo de 13|91|No es un pájaro\es un avión
+Pregunta = la casa del mayor múltiplo de 23|92|Es una casa celeste\de ventana amarilla
+Pregunta = la casa del mayor múltiplo de 31|93|Es la tercera de\la última fila
+Pregunta = la casa del múltiplo de 47 que es par|94|Es una casa azul de\ventanas fucsias
+Pregunta = la casa del mayor múltiplo de 19|95|Es la mas pequeña\que termina en 5
+Pregunta = la casa del mayor múltiplo de 12|96|Está junto al último\zapato y es verde
+Pregunta = la casa del más grande de los primos|97|El único zapato de\la última fila
+Pregunta = la casa del mayor múltiplo de 7|98|Es la última de la\columna de los\jarrones
+Pregunta = la casa del mayor múltiplo de 11|99|Si la veo al revés es 66
+Pregunta = la casa del mayor múltiplo de 5|100|Es negra y es par
+Pregunta = la casa del primer impar|1|Es el mas chico de todos
+Pregunta = la casa del primo número 1|2|Es el primero con un zapato
+Pregunta = la casa del primo número 2|3|Es el segundo con un zapato
+Pregunta = la casa del primo número 3|5|Es el tercero con un zapato
+Pregunta = la casa del primo número 4|7|Es un triángulo y\está junto a la casa Jarrón
+Pregunta = la casa del primo número 5|11|El primer zapato\de la segunda fila
+Pregunta = la casa del primo número 6|13|Es la tetera que tiene\el sexto zapato
+Pregunta = la casa del primo número 7|17|Segundo zapato de\la segunda fila
+Pregunta = la casa del primo número 8|19|Verde alta y con zapato
+Pregunta = la casa del primo número 9|23| Es la casa latita
+Pregunta = la casa del primo número 10|29|Tiene zapato tipo\champión en la puerta
+Pregunta = la casa del primo número 11|31|Es rosada y tiene\zapato
+Pregunta = la casa del primo número 12|37|Es un helicóptero con zapato
+Pregunta = la casa del primo número 13|41|Tiene el zapato de\los primos y está junto al cohete
+Pregunta = la casa del primo número 14|43|Tiene zapato rojo de\la quinta fila
+Pregunta = la casa del primo número 15|47|Está dentro de una valija
+Pregunta = la casa del primo número 16|53|Está arriba de una tetera\pero no al lado de otra con zapato
+Pregunta = la casa del primo número 17|59|Está junto al jarrón\y tiene un zapato
+Pregunta = la casa del primo número 18|61|Es negra, es impar\y no es el 11
+Pregunta = la casa del primo número 19|67|Tiene un zapato\y una estrella
+Pregunta = la casa del primo número 20|71|Tiene zapato y está\junto a una flor
+Pregunta = la casa del primo número 21|73|Es la casa latita
+Pregunta = la casa del primo número 22|79|Tiene zapato tipo\champión en la puerta
+Pregunta = la casa del primo número 23|83|Tiene zapato, techo\ amarilloy es roja
+Pregunta = la casa del primo número 24|89|Está junto a la manzana\ y es mayor que 50
+Pregunta = la casa del primo número 25|97|Es el único zapato de\la última fila
+Pregunta = la casa del neutro de la multiplicación|1|Es el mas chico de todos
+Pregunta = la casa del primo que es par|2|Es el primero con un zapato
+Pregunta = la casa del primo que sigue a otro primo|3|Es el segundo con un zapato
+Pregunta = la casa del número de vértices del cuadrado|4|Es un edificio azul\sobre la primer fila
+Pregunta = la casa del número de vértices del pentágono|5|Es el tercero con un zapato
+Pregunta = la casa del número de vértices del hexágono|6|Es verde, está en la\la primer fila y tiene ventanas amarillas
+Pregunta = la casa del número de vértices del heptágono|7|Es un triángulo y\está junto a la casa Jarrón
+Pregunta = la casa del primo gemelo de 13|11|El primer zapato\de la segunda fila
+Pregunta = la casa del primo gemelo de 11|13|Es la tetera que tiene\ el sexto zapato
+Pregunta = la casa del primo gemelo de 19|17|Segundo zapato de\la segunda fila
+Pregunta = la casa del primo gemelo de 17|19|Verde alta y con zapato
+Pregunta = la casa de la veintena|20|Es una torre inclinada
+Pregunta = la casa del primo gemelo de 31|29|Tiene zapato tipo\champión en la puerta
+Pregunta = la casa del primo gemelo de 29|31|Es rosada y tiene\zapato
+Pregunta = la casa del primo gemelo de 43|41|Tiene el zapato de\los primos y está junto al cohete
+Pregunta = la casa del primo gemelo de 41|43|Tiene zapato rojo de\la quinta fila
+Pregunta = la casa del primer múltiplo de 4 y de 11|44|Es la rosada de\ la tabla del 11
+Pregunta = la casa del primer múltiplo de 25 y de 2|50|Tiene el techo de embudo
+Pregunta = la casa del primo gemelo de 61|59|Está junto al jarrón\y tiene un zapato
+Pregunta = la casa del primo gemelo de 59|61|Es negra, es impar \y no es el 11
+Pregunta = la casa del primo gemelo de 73|71|Tiene zapato y está\junto a una flor
+Pregunta = la casa del primo gemelo de 71|73|Es la casa latita
+Pregunta = la casa de la centena|100|Es negra y es par
+
+
+
+
+
diff --git a/recursos/lamina/state_icon.png b/recursos/lamina/state_icon.png
new file mode 100644
index 0000000..223c5b4
--- /dev/null
+++ b/recursos/lamina/state_icon.png
Binary files differ
diff --git a/recursos/lamina/terron.png b/recursos/lamina/terron.png
new file mode 100644
index 0000000..375a8aa
--- /dev/null
+++ b/recursos/lamina/terron.png
Binary files differ
diff --git a/recursos/lamina/zona_definiciones.txt~ b/recursos/lamina/zona_definiciones.txt~
new file mode 100644
index 0000000..6613426
--- /dev/null
+++ b/recursos/lamina/zona_definiciones.txt~
@@ -0,0 +1,35 @@
+0, Baja California
+5, Baja California Sur
+10, Sonora
+15, Chihuahua
+20, Coahuila
+25, Sinaloa
+30, Durango
+35, Nuevo Leon
+40, Tamaulipas
+45, Zacatecas
+50, Nayarit
+55, San Luis Potosí
+60, Aguascalientes
+65, Jalisco
+70, Guanajuato
+75, Querétaro
+80, Veracruz
+85, Colima
+90, Hidalgo
+95, Michoacán
+100, Estado de México
+105, Guerrero
+110, Morelia
+115, Tlaxcala
+120, Puebla
+125, Oaxaca
+130, Distrito Federal
+135, Tabasco
+140, Chiapas
+145, Campeche
+150, Yucatán
+155, Quintana Roo
+
+
+
diff --git a/recursos/lamina/zonas.png b/recursos/lamina/zonas.png.backup
index c0ef0d4..c0ef0d4 100644
--- a/recursos/lamina/zonas.png
+++ b/recursos/lamina/zonas.png.backup
Binary files differ
diff --git a/recursos/lamina/zonas.txt b/recursos/lamina/zonas.txt.backup
index e281474..e281474 100644
--- a/recursos/lamina/zonas.txt
+++ b/recursos/lamina/zonas.txt.backup
diff --git a/recursos/lamina/zonas.txt~ b/recursos/lamina/zonas.txt~
new file mode 100644
index 0000000..b12793f
--- /dev/null
+++ b/recursos/lamina/zonas.txt~
@@ -0,0 +1,32 @@
+1|160|0|0|0
+2|5|0|0|0
+3|10|0|0|0
+4|15|0|0|0
+5|20|0|0|0
+6|25|0|0|0
+7|30|0|0|0
+8|35|0|0|0
+9|40|0|0|0
+10|45|0|0|0
+11|50|0|0|0
+12|55|0|0|0
+13|60|0|0|0
+14|65|0|0|0
+15|70|0|0|0
+16|75|0|0|0
+17|80|0|0|0
+18|85|0|0|0
+19|90|0|0|0
+20|95|0|0|0
+21|100|0|0|0
+22|105|0|0|0
+23|110|0|0|0
+24|115|0|0|0
+25|120|0|0|0
+26|125|0|0|0
+27|130|0|0|0
+28|135|0|0|0
+29|140|0|0|0
+30|145|0|0|0
+31|150|0|0|0
+32|155|0|0|0
diff --git a/recursos/lamina/zone_definitiones_capitals.txt b/recursos/lamina/zone_definitiones_capitals.txt
new file mode 100644
index 0000000..999252b
--- /dev/null
+++ b/recursos/lamina/zone_definitiones_capitals.txt
@@ -0,0 +1,33 @@
+160, Mexicali
+5, La Paz
+10, Hermosillo
+15, Chihuahua
+20, Saltillo
+25, Culiacan
+30, Durango
+35, Monterrey
+40, Ciudad Victoria
+45, Zacatecas
+50, Tepic
+55, San Luis Potosí
+60, Aguascalientes
+65, Guadalajara
+70, Guanajuato
+75, Querétaro
+80, Jalapa
+85, Colima
+90, Pachuca
+95, Morelia
+100, Toluca
+105, Chilpancingo
+110, Cuernavaca
+115, Tlaxcala
+120, Puebla
+125, Oaxaca
+130, Mexico
+135, Villahermosa
+140, Tuxtla Gutierrez
+145, Campeche
+150, Merida
+155, Chetumal
+
diff --git a/recursos/lamina/zone_definitiones_states.txt b/recursos/lamina/zone_definitiones_states.txt
new file mode 100644
index 0000000..e129bf7
--- /dev/null
+++ b/recursos/lamina/zone_definitiones_states.txt
@@ -0,0 +1,35 @@
+160, Baja California
+5, Baja California Sur
+10, Sonora
+15, Chihuahua
+20, Coahuila
+25, Sinaloa
+30, Durango
+35, Nuevo Leon
+40, Tamaulipas
+45, Zacatecas
+50, Nayarit
+55, San Luis Potosí
+60, Aguascalientes
+65, Jalisco
+70, Guanajuato
+75, Querétaro
+80, Veracruz
+85, Colima
+90, Hidalgo
+95, Michoacán
+100, Estado de México
+105, Guerrero
+110, Morelos
+115, Tlaxcala
+120, Puebla
+125, Oaxaca
+130, Distrito Federal
+135, Tabasco
+140, Chiapas
+145, Campeche
+150, Yucatán
+155, Quintana Roo
+
+
+
diff --git a/recursos/lamina/zones_capitals.png b/recursos/lamina/zones_capitals.png
new file mode 100644
index 0000000..94ff1ae
--- /dev/null
+++ b/recursos/lamina/zones_capitals.png
Binary files differ
diff --git a/recursos/lamina/zones_capitals.txt b/recursos/lamina/zones_capitals.txt
new file mode 100644
index 0000000..ff31df2
--- /dev/null
+++ b/recursos/lamina/zones_capitals.txt
@@ -0,0 +1,32 @@
+1|160
+2|5
+3|10
+4|15
+5|20
+6|25
+7|30
+8|35
+9|40
+10|45
+11|50
+12|55
+13|60
+14|65
+15|70
+16|75
+17|80
+18|85
+19|90
+20|95
+21|100
+22|105
+23|110
+24|115
+25|120
+26|125
+27|130
+28|135
+29|140
+30|145
+31|150
+32|155
diff --git a/recursos/lamina/zones_scaled.png b/recursos/lamina/zones_scaled.png
new file mode 100644
index 0000000..836b208
--- /dev/null
+++ b/recursos/lamina/zones_scaled.png
Binary files differ
diff --git a/recursos/lamina/zones_states.png b/recursos/lamina/zones_states.png
new file mode 100644
index 0000000..37c5e2b
--- /dev/null
+++ b/recursos/lamina/zones_states.png
Binary files differ
diff --git a/recursos/lamina/zones_states.txt b/recursos/lamina/zones_states.txt
new file mode 100644
index 0000000..ff31df2
--- /dev/null
+++ b/recursos/lamina/zones_states.txt
@@ -0,0 +1,32 @@
+1|160
+2|5
+3|10
+4|15
+5|20
+6|25
+7|30
+8|35
+9|40
+10|45
+11|50
+12|55
+13|60
+14|65
+15|70
+16|75
+17|80
+18|85
+19|90
+20|95
+21|100
+22|105
+23|110
+24|115
+25|120
+26|125
+27|130
+28|135
+29|140
+30|145
+31|150
+32|155
diff --git a/recursos/lamina/zones_states_bad.png b/recursos/lamina/zones_states_bad.png
new file mode 100644
index 0000000..3f173cc
--- /dev/null
+++ b/recursos/lamina/zones_states_bad.png
Binary files differ