changeset 205:54bfd1015b35

* PyChan event handling rework (part I) ** Unified listeners ** ...hopefully more robust attach/detach code. * Added compat, layout and also the new autopsition feature. * Documentation * Minor style fixes in core.
author phoku@33b003aa-7bff-0310-803a-e67f0ece8222
date Sat, 14 Mar 2009 12:13:29 +0000
parents 5816ab527da8
children 6214a0b91eb2
files clients/pychan_demo/pychan_test.py doc/dependencies/dirdeps.dot doc/dependencies/filedeps.dot engine/core/gui/guilistener.i engine/core/util/base/resourceclass.cpp engine/core/util/resource/pool.h engine/core/video/animation.cpp engine/extensions/pychan/__init__.py engine/extensions/pychan/autoposition.py engine/extensions/pychan/compat.py engine/extensions/pychan/dialogs.py engine/extensions/pychan/events.py engine/extensions/pychan/exceptions.py engine/extensions/pychan/fonts.py engine/extensions/pychan/internal.py engine/extensions/pychan/layout.py engine/extensions/pychan/tools.py engine/extensions/pychan/widgets.py
diffstat 18 files changed, 1120 insertions(+), 136 deletions(-) [+]
line wrap: on
line diff
--- a/clients/pychan_demo/pychan_test.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/clients/pychan_demo/pychan_test.py	Sat Mar 14 12:13:29 2009 +0000
@@ -38,7 +38,7 @@
 
 def testTimer():
 	import timer
-	timer.init( pychan.manager.engine.getTimeManager() )
+	timer.init( pychan.manager.hook.engine.getTimeManager() )
 	def spam():
 		print "SPAM SPAM"
 		return 1
--- a/doc/dependencies/dirdeps.dot	Sat Mar 14 12:03:56 2009 +0000
+++ b/doc/dependencies/dirdeps.dot	Sat Mar 14 12:13:29 2009 +0000
@@ -190,6 +190,7 @@
     "model/structures" -> "util/math"
     "model/structures" -> "util/resource"
     "model/structures" -> "util/structures"
+    "model/structures" -> "util/time"
     "pathfinder" -> "model/metamodel"
     "pathfinder" -> "model/structures"
     "pathfinder" -> "util/base"
@@ -263,6 +264,7 @@
     "view/renderers" -> "util/log"
     "view/renderers" -> "util/math"
     "view/renderers" -> "util/structures"
+    "view/renderers" -> "util/time"
     "view/renderers" -> "video"
     "view/renderers" -> "video/fonts"
     "view/renderers" -> "video/sdl"
--- a/doc/dependencies/filedeps.dot	Sat Mar 14 12:03:56 2009 +0000
+++ b/doc/dependencies/filedeps.dot	Sat Mar 14 12:13:29 2009 +0000
@@ -165,6 +165,7 @@
     "engine/core/gui/widgets/clicklabel.cpp" -> "util/base/exception.h"
     "engine/core/gui/widgets/clicklabel.cpp" -> "video/image.h"
     "engine/core/gui/widgets/icon2.cpp" -> "icon2.hpp"
+    "engine/core/gui/widgets/togglebutton.cpp" -> "togglebutton.h"
     "engine/core/gui/widgets/twobutton.cpp" -> "twobutton.h"
     "engine/core/loaders/native/audio_loaders/ogg_loader.cpp" -> "audio/soundclip.h"
     "engine/core/loaders/native/audio_loaders/ogg_loader.cpp" -> "ogg_loader.h"
@@ -248,6 +249,7 @@
     "engine/core/model/structures/instance.cpp" -> "util/base/exception.h"
     "engine/core/model/structures/instance.cpp" -> "util/log/logger.h"
     "engine/core/model/structures/instance.cpp" -> "util/math/fife_math.h"
+    "engine/core/model/structures/instance.cpp" -> "util/time/timemanager.h"
     "engine/core/model/structures/instance.h" -> "location.h"
     "engine/core/model/structures/instance.h" -> "model/metamodel/abstractvisual.h"
     "engine/core/model/structures/instance.h" -> "model/metamodel/object.h"
@@ -636,6 +638,7 @@
     "engine/core/view/renderers/genericrenderer.cpp" -> "model/structures/location.h"
     "engine/core/view/renderers/genericrenderer.cpp" -> "util/log/logger.h"
     "engine/core/view/renderers/genericrenderer.cpp" -> "util/math/fife_math.h"
+    "engine/core/view/renderers/genericrenderer.cpp" -> "util/time/timemanager.h"
     "engine/core/view/renderers/genericrenderer.cpp" -> "video/animation.h"
     "engine/core/view/renderers/genericrenderer.cpp" -> "video/animationpool.h"
     "engine/core/view/renderers/genericrenderer.cpp" -> "video/fonts/abstractfont.h"
--- a/engine/core/gui/guilistener.i	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/core/gui/guilistener.i	Sat Mar 14 12:13:29 2009 +0000
@@ -22,36 +22,17 @@
 %module fife
 %{
 #include <guichan.hpp>
-#include "gui/guilistener.h"
+#include <guichan/actionevent.hpp>
+#include <guichan/keyevent.hpp>
 %}
 
 namespace gcn {
 
-	%nodefaultctor;
+	%feature("director") MouseListener;
 	class MouseListener {
 	public:
-	};
-
-	class KeyListener {
-	public:
-	};
+		virtual ~MouseListener();
 
-	class ActionListener {
-	public:
-	};
-	%clearnodefaultctor;
-}
-
-namespace FIFE {
-
-	%feature("director") GUIEventListener;
-	class GUIEventListener : 
-		public gcn::MouseListener, public gcn::KeyListener, public gcn::ActionListener {
-	public:
-		 GUIEventListener();
-		 virtual ~GUIEventListener();
-
-		/* Mouse events */
 		virtual void mouseEntered(gcn::MouseEvent& mouseEvent);
 		virtual void mouseExited(gcn::MouseEvent& mouseEvent);
 		virtual void mousePressed(gcn::MouseEvent& mouseEvent);
@@ -61,9 +42,27 @@
 		virtual void mouseWheelMovedDown(gcn::MouseEvent& mouseEvent);
 		virtual void mouseMoved(gcn::MouseEvent& mouseEvent);
 		virtual void mouseDragged(gcn::MouseEvent& mouseEvent);
-
-		virtual void action(const gcn::ActionEvent& actionEvent);
-
+	protected:
+		MouseListener() { }
 	};
 
+	%feature("director") MouseListener;
+	class KeyListener {
+	public:
+		virtual ~KeyListener() { }
+
+		virtual void keyPressed(gcn::KeyEvent& keyEvent) { }
+		virtual void keyReleased(gcn::KeyEvent& keyEvent) { }
+	protected:
+		KeyListener() { }
+	};
+
+	%feature("director") ActionListener;
+	class ActionListener {
+	public:
+		virtual ~ActionListener() { }
+		virtual void action(const gcn::ActionEvent& actionEvent) = 0;
+	};
 }
+
+
--- a/engine/core/util/base/resourceclass.cpp	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/core/util/base/resourceclass.cpp	Sat Mar 14 12:13:29 2009 +0000
@@ -56,7 +56,6 @@
 	
 	void ResourceClass::setResourceLocation(const ResourceLocation& location) {
 		delete m_location;
-		m_location = NULL;
 		m_location = location.clone();
 	}
 	
--- a/engine/core/util/resource/pool.h	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/core/util/resource/pool.h	Sat Mar 14 12:13:29 2009 +0000
@@ -130,7 +130,6 @@
 		 */
 		virtual void reset();
 
-	protected:
 	private:
 		class PoolEntry {
 		public:
--- a/engine/core/video/animation.cpp	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/core/video/animation.cpp	Sat Mar 14 12:13:29 2009 +0000
@@ -46,16 +46,9 @@
 		}
 	
 	Animation::~Animation() {
-// 		std::vector<FrameInfo>::const_iterator i(m_frames.begin());
-// 		while (i != m_frames.end()) {
-// 			i->img->decRef();
-// 			i++;
-// 		}
+		// note: we don't need to free the images, as they are handled via
+		// smart references.
 	}
-	
-// 	void Animation::addFrame(Image* image, unsigned int duration) {
-// 		addFrame(ResourcePtr(image),duration);
-// 	}
 
 	void Animation::addFrame(ResourcePtr image, unsigned int duration) {
 		FrameInfo info;
--- a/engine/extensions/pychan/__init__.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/extensions/pychan/__init__.py	Sat Mar 14 12:13:29 2009 +0000
@@ -10,7 +10,7 @@
 --------
  - Simpler Interface
  - Very Basic XML Format support
- - Very Basic Layout Engine
+ - Basic Layout Engine
  - Pseudo-Synchronous Dialogs.
  - Automagic background tiling (WIP)
  - Basic Styling support.
@@ -24,8 +24,6 @@
  - Documentation ( Allways not enough :-( )
  - Handle Image Fonts
  - Move Font config files to XML, too ...
- - Add support for fixed size 'Spacers'
- - Add messageBox(text)
 
  - Implement real Menus
  - Implement StackWidget
@@ -37,9 +35,9 @@
 BUGS
 ----
  - Focus problems with Widget.execute.
- - Layout Bugs where max_size of a parent widget get's ignored.
  - Font.glyph_spacing is rendered incorrectly.
- - It just looks ugly.
+ - Is this a bug? At least inconvenient. MouseEntered events are not distributed for freshly shown widget.
+ - It just looks bad.
 
 Problems
 --------
@@ -154,6 +152,8 @@
      }
   }
 
+A new style is added to pychan with L{internal.Manager.addStyle}.
+
 The font is set via a string identifier pulled from a font definition
 in a PyChan configuration file. You have to load these by calling
 L{loadFonts} in your startup code::
@@ -225,8 +225,6 @@
 	'manager'
 ]
 
-import fife, pythonize
-
 from widgets import *
 from exceptions import *
 
@@ -244,9 +242,11 @@
 
 	@param engine: The FIFE engine object.
 	"""
-	from manager import Manager
+	from compat import _munge_engine_hook
+	from internal import Manager
 	global manager
-	manager = Manager(engine,debug)
+
+	manager = Manager(_munge_engine_hook(engine),debug)
 
 # XML Loader
 
@@ -326,6 +326,9 @@
 
 	def _createSpacer(self,cls,name,attrs):
 		obj = cls(parent=self.root)
+		for k,v in attrs.items():
+			self._setAttr(obj,k,v)
+
 		if hasattr(self.root,'add'):
 			self.root.addSpacer(obj)
 		else:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/extensions/pychan/autoposition.py	Sat Mar 14 12:13:29 2009 +0000
@@ -0,0 +1,121 @@
+# coding: utf-8
+
+"""
+Automatic widget positioning
+============================
+
+You can use the C{position_techinque} attribute
+on top level widgets which can also be set from xml.
+
+For direct use call L{placeWidget}.
+"""
+
+from internal import screen_width, screen_height
+from exceptions import PyChanException
+
+EXPLICIT = "explicit"
+AUTOMATIC = "automatic"
+
+TOP = "top"
+LEFT = "left"
+RIGHT = "right"
+CENTER = "center"
+BOTTOM = "bottom"
+
+
+def _splicePosition(p):
+	if "+" in p:
+		technique,delta = p.split("+")
+	elif "-" in p:
+		technique,delta = p.split("-")
+		delta = '-' + delta
+	else:
+		technique,delta = p,0
+	delta = int(delta)
+	return technique,delta
+
+def _parsePosition(position):
+	try:
+		if position == AUTOMATIC:
+			position = "center+0:center+0"
+
+		x_pos,y_pos = position.split(":")
+		x_pos, x_delta = _splicePosition(x_pos)
+		y_pos, y_delta = _splicePosition(y_pos)
+
+		if x_pos not in [EXPLICIT,LEFT,CENTER,RIGHT]:
+			raise 
+		if y_pos not in [EXPLICIT,TOP,CENTER,BOTTOM]:
+			raise
+	except:
+		raise PyChanException("Malformed position definition: " + repr(position))
+	return x_pos,x_delta,y_pos,y_delta
+
+def placeWidget(widget,position):
+	"""
+	Place a widget according to a string defining relative coordinates to screen borders.
+
+	The position definition has to be of the form: C{"<x_pos><x_delta>:<y_pos><y_delta>"}
+
+	C{<x_pos>} may be one of:
+          - left
+          - right
+          - center
+          - explicit
+
+	C{<y_pos>} may be one of:
+          - top
+          - bottom
+          - center
+          - explicit
+
+        C{explicit} means that the widgets x or y position will not be touched. The rest should be
+        self explanatory.
+
+	C{<x_delta>} and C{<y_delta>} must be of the form: +pixel_number or -pixel_number. Or comletely
+        omitted. Note that the sign has to be there for for positive deltas, too.
+
+	For brevity two shortcuts exist:
+          - "explict" -> "explict:explict"
+          - "automatic" -> "center:center"
+        
+	A few examples::
+          "right-20:top"
+          "center:top+10"
+          "center:center"
+
+	@param widget: The PyChan widget.
+	@param position: A position definition.
+
+	If the position cannot be parsed a L{PyChanException} is thrown.
+
+	"""
+	if position == EXPLICIT:
+		return
+	x_pos,x_delta,y_pos,y_delta = _parsePosition(position)
+
+	x,y = widget.position
+	w,h = widget.size
+
+	if x_pos == CENTER:
+		x = (screen_width()-w)/2 + x_delta
+
+	if y_pos == CENTER:
+		y = (screen_height()-h)/2 + y_delta
+
+	if x_pos == LEFT:
+		x = x_delta
+
+	if y_pos == TOP:
+		y = y_delta
+
+	if x_pos == RIGHT:
+		x = screen_width() - w + x_delta
+
+	if y_pos == BOTTOM:
+		y = screen_height() - h + y_delta
+
+	widget.position = x,y
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/extensions/pychan/compat.py	Sat Mar 14 12:13:29 2009 +0000
@@ -0,0 +1,87 @@
+# coding: utf-8
+
+in_fife = None
+guichan = None
+
+def _import_guichan():
+	global in_fife
+
+	err_fife = ""
+	try:
+		import fife
+		in_fife = True
+		return fife
+	except ImportError, e:
+		err_fife = str(e)
+	
+	try:
+		import guichan
+		in_fife = False
+		return guichan
+	except ImportError, e:
+		import traceback
+		traceback.print_exc()
+		raise ImportError("Couldn't import neither fife nor guichan: fife:'%s' guichan:'%s'" % (err_fife,str(e)))
+guichan = _import_guichan()
+
+
+
+def _munge_engine_hook(engine):
+	engine.translate_mouse_event = getattr(engine,'translate_mouse_event',lambda x : x )
+	engine.translate_key_event   = getattr(engine,'translate_key_event',lambda x : x )
+
+	if not in_fife:
+		return engine
+	if not isinstance(engine,fife.Engine):
+		return engine
+
+	guimanager = engine.getGuiManager()
+
+	def _fife_load_image(filename):
+		index = engine.imagePool.addResourceFromFile(filename)
+		return guichan.GuiImage(index,engine.getImagePool())
+
+	class hook:
+		pass
+	hook = hook()
+
+	hook.add_widget    = guimanager.add
+	hook.remove_widget = guimanager.remove
+	hook.default_font  = engine.getDefaultFont()
+	hook.load_image    = _fife_load_image
+	hook.translate_mouse_event = guimanager.translateMouseEvent
+	hook.translate_key_event   = guimanager.translateKeyEvent
+
+	hook.screen_width  = engine.getRenderBackend().getScreenWidth()
+	hook.screen_height = engine.getRenderBackend().getScreenHeight()
+
+	hook.engine        = engine
+	return hook
+
+
+class _multilistener(guichan.ActionListener,guichan.MouseListener,guichan.KeyListener):
+	def __init__(self):
+		guichan.ActionListener.__init__(self)
+		guichan.MouseListener.__init__(self)
+		guichan.KeyListener.__init__(self)
+
+
+class _point(object):
+	def __init__(self,x=0,y=0):
+		self.x=0
+		self.y=0
+
+if in_fife:
+	fife = guichan
+	guichan.ActionListener._ActionListener_init__ = lambda x : x
+	#guichan.MouseListener.__init__ = lambda x : x
+	guichan.KeyListener.__init__ = lambda x : x
+else:
+	guichan.Point = _point
+	guichan.ScrollArea.SHOW_AUTO = guichan.ScrollArea.ShowAuto
+	guichan.ScrollArea.SHOW_NEVER = guichan.ScrollArea.ShowNever
+	guichan.ScrollArea.SHOW_ALWAYS = guichan.ScrollArea.ShowAlways
+
+assert isinstance(_multilistener(),guichan.ActionListener)
+assert isinstance(_multilistener(),guichan.MouseListener)
+assert isinstance(_multilistener(),guichan.KeyListener)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/extensions/pychan/dialogs.py	Sat Mar 14 12:13:29 2009 +0000
@@ -0,0 +1,197 @@
+# coding: utf-8
+
+from pychan import loadXML
+import pychan.tools
+import widgets
+from internal import get_manager, screen_width, screen_height
+from StringIO import StringIO
+
+OK,YES,NO,CANCEL = True,True,False,None
+
+def print_event(**kwargs):
+	print kwargs
+
+class XMLDialog(object):
+	def __init__(self, xml, ok_field = None, cancel_field = None,initial_data={},data={}):
+		self.gui = loadXML(xml)
+		self.ok_field = ok_field
+		self.cancel_field = cancel_field
+		self.initial_data= initial_data
+		self.data= data
+		self.max_size=None
+		self.min_size=None
+		self.gui.capture(print_event,"mouseEntered")
+
+	def execute(self):
+		self.gui.distributeInitialData(self.initial_data)
+		self.gui.distributeData(self.data)
+
+		screen_w, screen_h = screen_width(), screen_height()
+		if self.max_size is None:
+			self.max_size = screen_w/2, screen_h/3
+		if self.min_size is None:
+			self.min_size = screen_w/2, screen_h/4
+		self.gui.max_size = self.max_size
+		self.gui.min_size = self.min_size
+
+		resultMap = {}
+		if self.gui.findChild(name="okButton"):
+			resultMap["okButton"] = OK
+
+		if self.gui.findChild(name="cancelButton"):
+			resultMap["cancelButton"] = CANCEL
+
+		if self.gui.findChild(name="yesButton"):
+			resultMap["noButton"] = NO
+
+		if self.gui.findChild(name="yesButton"):
+			resultMap["yesButton"] = YES
+
+		ok = self.gui.execute(resultMap)
+		if ok:
+			return self.getOkResult()
+		return self.getCancelResult()
+
+	def getOkResult(self):
+		if self.ok_field:
+			return self.gui.collectData(self.ok_field)
+		return True
+
+	def getCancelResult(self):
+		if self.cancel_field:
+			return self.gui.collectData(self.cancel_field)
+		return False
+
+MESSAGE_BOX_XML = """\
+<Window name="window" title="Message">
+<ScrollArea>
+<Label wrap_text="1" text="$MESSAGE" name="message" vexpanding="1"/>
+</ScrollArea>
+<HBox>
+<Spacer/><Button min_width="50" name="okButton" text="OK"/>
+</HBox>
+</Window>
+"""
+
+YESNO_BOX_XML = """\
+<Window name="window" title="Question">
+<ScrollArea>
+<Label wrap_text="1" text="$MESSAGE" name="message" vexpanding="1"/>
+</ScrollArea>
+<HBox>
+<Spacer/>
+<Button min_width="50" name="yesButton" text="Yes"/>
+<Button min_width="50" name="noButton" text="No"/>
+</HBox>
+</Window>
+"""
+
+YESNOCANCEL_BOX_XML = """\
+<Window name="window" title="Question">
+<ScrollArea>
+<Label wrap_text="1" text="$MESSAGE" name="message" vexpanding="1"/>
+</ScrollArea>
+<HBox>
+<Spacer/>
+<Button min_width="50" name="yesButton" text="Yes"/>
+<Button min_width="50" name="noButton" text="No"/>
+<Button min_width="50" name="cancelButton" text="Cancel"/>
+</HBox>
+</Window>
+"""
+
+SELECT_BOX_XML = """\
+<Window name="window" title="Select">
+<Label wrap_text="1" text="$MESSAGE" name="message"/>
+<ScrollArea>
+<ListBox name="selection">
+</ListBox>
+</ScrollArea>
+<HBox>
+<Spacer/>
+<Button min_width="50" name="okButton" text="Select"/>
+<Button min_width="50" name="cancelButton" text="Cancel"/>
+</HBox>
+</Window>
+"""
+
+EXCEPTION_CATCHER_XML="""\
+<Window name="window" title="An exception occurred - what now?">
+  <VBox hexpanding="1">
+    <Label wrap_text="1" max_width="400" text="$MESSAGE" name="message"/>
+    <ScrollArea>
+    <Label text="$MESSAGE" name="traceback"/>
+    </ScrollArea>
+  </VBox>
+  <HBox>
+    <Spacer/>
+    <Button name="yesButton" text="Retry"/>
+    <Button name="noButton" text="Ignore"/>
+    <Button name="cancelButton" text="Reraise"/>
+  </HBox>
+</Window>
+"""
+
+def _make_text(message):
+	if callable(message):
+		message = message()
+	if hasattr(message,"read"):
+		message = message.read()
+	return message
+
+def message(message="",caption="Message"):
+	text = _make_text(message)
+	dialog = XMLDialog(StringIO(MESSAGE_BOX_XML),
+		initial_data={'message':text,'window':caption})
+	dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50
+	dialog.execute()
+
+def yesNo(message="",caption="Message"):
+	text = _make_text(message)
+	dialog = XMLDialog(StringIO(YESNO_BOX_XML),
+		initial_data={'message':text,'window':caption})
+	dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50
+	return dialog.execute()
+
+def yesNoCancel(message="",caption="Message"):
+	text = _make_text(message)
+	dialog = XMLDialog(StringIO(YESNOCANCEL_BOX_XML),
+		initial_data={'message':text,'window':caption})
+	dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50
+	return dialog.execute()
+
+def select(message="",options=[],caption="Message"):
+	text = _make_text(message)
+	dialog = XMLDialog(StringIO(SELECT_BOX_XML),
+		initial_data={'message':text,'window':caption})
+	dialog.size = screen_width()/3, 2*screen_height()/3
+
+	dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50
+	listbox = dialog.gui.findChild(name="selection")
+	listbox.items = options
+	if dialog.execute():
+		return listbox.selected_item
+	return None
+
+def trace(f):
+	import sys, traceback
+	def new_f(*args,**kwargs):
+		try:
+			return pychan.tools.applyOnlySuitable(f,*args,**kwargs)
+
+		except Exception, e:
+			dialog = XMLDialog(StringIO(EXCEPTION_CATCHER_XML),
+			  initial_data={'message':str(e)}
+			)
+			tb = traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)
+			dialog.gui.findChild(name="traceback").text = "".join(tb)
+			dialog.min_size = screen_width()/2,3*screen_height()/4
+			dialog.max_size = screen_width()/2,3*screen_height()/4
+			result = dialog.execute()
+			if result == YES:
+				return new_f(*args,**kwargs)
+			elif result == NO:
+				return
+			raise
+	return new_f
+
--- a/engine/extensions/pychan/events.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/extensions/pychan/events.py	Sat Mar 14 12:13:29 2009 +0000
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 #coding: utf-8
 
 """
@@ -11,15 +12,39 @@
 Nevertheless to understand how its supposed to work
 take a look at L{EventMapper} and L{EventListener}
 
+Event callbacks
+---------------
+
+You can either write callbacks yourself or
+use L{tools.callBackWithArguments} or L{tools.attrSetCallback}
+to generate suitable callbacks.
+
+Here's an example callback::
+   def dumpEventInfo(event=0,widget=0):
+      print widget, " received the event ", event
+
+Note the signature - C{event} and C{widget} are keyword
+arguments passed to the callback. If doesn't accept either
+C{event} or C{widget} as argument, these are not passed.
+
+This way a simple function which ignores C{event} or C{widget}
+can be used, while they are available if needed.
+
+Currently only one callback can be set per event. In case
+you don't want to write your own callback that dispatches
+to different callbacks you can use L{tools.chainCallbacks}.
+
 Available Events
 ----------------
 
 """
 
-import fife
+from compat import guichan
+
 import exceptions
-import manager
+from internal import get_manager
 import tools
+import traceback
 
 EVENTS = [
 	"mouseEntered",
@@ -37,6 +62,11 @@
 # Add the EVENTS to the docs.
 __doc__ += "".join([" - %s\n" % event for event in EVENTS])
 
+# The line before seems to leak the variable event into the global namespace ... remove that!
+# This is a python problem, addressed in python3
+try: del event
+except:pass
+
 MOUSE_EVENT, KEY_EVENT, ACTION_EVENT = range(3)
 def getEventType(name):
 	if "mouse" in name:
@@ -50,8 +80,7 @@
 You passed None as parameter to %s.capture, which would normally remove a mapped event.
 But there was no event mapped. Did you accidently call a function instead of passing it?
 """
-
-class EventListener(fife.GUIEventListener):
+class EventListenerBase(object):
 	"""
 	Redirector for event callbacks.
 	Use *only* from L{EventMapper}.
@@ -60,45 +89,92 @@
 	virtual methods are called from C++ to - listen to
 	Guichan events.
 
-	When the module is first loaded the event handler
-	methods are auto-generated from the list L{EVENTS}.
-	This is effectively the same code as::
-	  def mouseEntered(self,event):
-	    self._redirectEvent("mouseEntered",event)
-
-	This way L{EVENTS} and the actually receivable events
-	are forced to be in sync.
 	"""
-	def __init__(self,debug=True):
-		super(EventListener,self).__init__()
+	def __init__(self):
+		super(EventListenerBase,self).__init__()
 		self.events = {}
 		self.indent = 0
-		self.debug = debug
-		self.guimanager = manager.Manager.manager.guimanager
+		self.debug = 1
+		self.is_attached = False
+
+	def attach(self,widget):
+		"""
+		Start receiving events.
+		No need to call this manually.
+		"""
+
+		if self.is_attached:
+			return
+		if not self.events:
+			return
+		if self.debug: print "Attach:",self
+		self.doAttach(widget.real_widget)
+		self.widget = widget
+		self.is_attached = True
+
+	def detach(self):
+		"""
+		Stop receiving events.
+		No need to call this manually.
+		"""
+		if not self.is_attached:
+			return
+		if self.debug: print "Detach:",self
+		self.doDetach(self.widget.real_widget)
+		self.widget = None
+		self.is_attached = False
 
 	def _redirectEvent(self,name,event):
 		self.indent += 4
-		event = self.translateEvent(getEventType(name), event)
-		if name in self.events:
-			if self.debug: print "-"*self.indent, name
-			for f in self.events[name].itervalues():
-				f( event )
-		self.indent -= 4
+		try:
+			event = self.translateEvent(getEventType(name), event)
+			if name in self.events:
+				if self.debug: print "-"*self.indent, name
+				for f in self.events[name].itervalues():
+					f( event )
+
+		except:
+			print name, event
+			traceback.print_exc()
+			raise
+
+		finally:
+			self.indent -= 4
 
 	def translateEvent(self,event_type,event):
 		if event_type == MOUSE_EVENT:
-			return self.guimanager.translateMouseEvent(event)
+			return get_manager().hook.translate_mouse_event(event)
 		if event_type == KEY_EVENT:
-			return self.guimanager.translateKeyEvent(event)
+			return get_manager().hook.translate_key_event(event)
 		return event
 
-def _redirect(name):
-	def redirectorFunc(self,event):
-		self._redirectEvent(name,event)
-	return redirectorFunc
+class _ActionEventListener(EventListenerBase,guichan.ActionListener):
+	def __init__(self):super(_ActionEventListener,self).__init__()
+	def doAttach(self,real_widget):	real_widget.addActionListener(self)
+	def doDetach(self,real_widget): real_widget.removeActionListener(self)
+
+	def action(self,e): self._redirectEvent("action",e)
+
+class _MouseEventListener(EventListenerBase,guichan.MouseListener):
+	def __init__(self):super(_MouseEventListener,self).__init__()
+	def doAttach(self,real_widget):	real_widget.addMouseListener(self)
+	def doDetach(self,real_widget): real_widget.removeMouseListener(self)
 
-for event_name in EVENTS:
-	setattr(EventListener,event_name,_redirect(event_name))
+	def mouseEntered(self,e): self._redirectEvent("mouseEntered",e)
+	def mouseExited(self,e): self._redirectEvent("mouseExited",e)
+	def mousePressed(self,e): self._redirectEvent("mousePressed",e)
+	def mouseReleased(self,e): self._redirectEvent("mouseReleased",e)
+	def mouseClicked(self,e): self._redirectEvent("mouseClicked",e)
+	def mouseMoved(self,e): self._redirectEvent("mouseMoved",e)
+	def mouseDragged(self,e): self._redirectEvent("mouseDragged",e)
+
+class _KeyEventListener(EventListenerBase,guichan.KeyListener):
+	def __init__(self):super(_KeyEventListener,self).__init__()
+	def doAttach(self,real_widget):	real_widget.addKeyListener(self)
+	def doDetach(self,real_widget): real_widget.removeKeyListener(self)
+
+	def keyPressed(self,e): self._redirectEvent("keyPressed",e)
+	def keyReleased(self,e): self._redirectEvent("keyReleased",e)
 
 class EventMapper(object):
 	"""
@@ -106,7 +182,7 @@
 	and derived classes.
 
 	Every PyChan widget has an L{EventMapper} instance
-	as attribute *event_mapper*.
+	as attribute B{event_mapper}.
 
 	This instance handles all necessary house-keeping.
 	Such an event mapper can be either *attached* or
@@ -124,44 +200,17 @@
 	def __init__(self,widget):
 		super(EventMapper,self).__init__()
 		self.widget = widget
-		self.listener = EventListener()
+		self.listener = {
+			KEY_EVENT    : _KeyEventListener(),
+			ACTION_EVENT : _ActionEventListener(),
+			MOUSE_EVENT  : _MouseEventListener(),
+		}
 		self.is_attached = False
-		self.debug = manager.Manager.manager.debug
+		self.debug = get_manager().debug
 
-	def __del__(self):
-		self.detach()
 	def __repr__(self):
 		return "EventMapper(%s)" % repr(self.widget)
 
-	def attach(self):
-		"""
-		Start receiving events.
-		No need to call this manually.
-		"""
-
-		if self.is_attached:
-			return
-		if not self.listener.events:
-			return
-		if self.debug: print "Attach:",self
-		self.widget.real_widget.addKeyListener( self.listener )
-		self.widget.real_widget.addMouseListener( self.listener )
-		self.widget.real_widget.addActionListener( self.listener )
-		self.is_attached = True
-
-	def detach(self):
-		"""
-		Stop receiving events.
-		No need to call this manually.
-		"""
-
-		if not self.is_attached:
-			return
-		if self.debug: print "Detach:",self
-		self.widget.real_widget.removeKeyListener( self.listener )
-		self.widget.real_widget.removeMouseListener( self.listener )
-		self.widget.real_widget.removeActionListener( self.listener )
-		self.is_attached = False
 
 	def capture(self,event_name,callback,group_name):
 		if event_name not in EVENTS:
@@ -169,32 +218,48 @@
 
 		if callback is None:
 			if self.isCaptured(event_name,group_name):
-				del self.listener.events[event_name][group_name]
-				if not self.listener.events[event_name]:
-					del self.listener.events[event_name]
-				if not self.listener.events:
-					self.detach()
+				self.removeEvent(event_name,group_name)
 			elif self.debug:
 				print CALLBACK_NONE_MESSAGE % str(self.widget)
 			return
+		self.addEvent(event_name,callback,group_name)
 
+	def isCaptured(self,event_name,group_name="default"):
+		return ("%s/%s" % (event_name,group_name)) in self.getCapturedEvents()
+
+	def getCapturedEvents(self):
+		events = []
+		for event_type, listener in self.listener.items():
+			for event_name, group in listener.events.items():
+				for group_name in group.keys():
+					events.append( "%s/%s" % (event_name, group_name) )
+		return events
+
+	def getListener(self,event_name):
+		return self.listener[getEventType(event_name)]
+
+	def removeEvent(self,event_name,group_name):
+		listener = self.getListener(event_name)
+		del listener.events[event_name][group_name]
+		if not listener.events[event_name]:
+			del listener.events[event_name]
+		if not listener.events:
+			listener.detach()
+
+	def addEvent(self,event_name,callback,group_name):
 		if not callable(callback):
 			raise RuntimeError("An event callback must be either a callable or None - not %s" % repr(callback))
 
 		def captured_f(event):
 			tools.applyOnlySuitable(callback,event=event,widget=self.widget)
 
-		if event_name not in self.listener.events:
-			self.listener.events[event_name] = {group_name : captured_f}
-		else:
-			self.listener.events[event_name][group_name] = captured_f
-		self.attach()
+		listener = self.getListener(event_name)
 
-	def isCaptured(self,event_name,group_name="default"):
-		return event_name in self.listener.events and group_name in self.listener.events[event_name]
-
-	def getCapturedEvents(self):
-		return self.listener.events.keys()
+		if event_name not in listener.events:
+			listener.events[event_name] = {group_name : captured_f}
+		else:
+			listener.events[event_name][group_name] = captured_f
+		listener.attach(self.widget)
 
 
 def splitEventDescriptor(name):
@@ -210,3 +275,4 @@
 		L = L[0],L[1],"default"
 	return L
 
+
--- a/engine/extensions/pychan/exceptions.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/extensions/pychan/exceptions.py	Sat Mar 14 12:13:29 2009 +0000
@@ -31,6 +31,6 @@
 
 class PrivateFunctionalityError(RuntimeError):
 	"""
-	Exception raised if private attributes/functions are used."
+	Exception raised if private attributes/functions are used.
 	"""
 	pass
--- a/engine/extensions/pychan/fonts.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/extensions/pychan/fonts.py	Sat Mar 14 12:13:29 2009 +0000
@@ -5,7 +5,7 @@
 
 class Font(object):
 	def __init__(self,name,get):
-		from manager import Manager
+		from internal import get_manager
 		self.font = None
 		self.name = name
 		self.typename = get("type")
@@ -17,7 +17,7 @@
 			self.size = int(get("size"))
 			self.antialias = int(get("antialias",1))
 			self.color = map(int,get("color","255,255,255").split(','))
-			self.font = Manager.manager.guimanager.createFont(self.source,self.size,"")
+			self.font = get_manager().hook.engine.getGuiManager().createFont(self.source,self.size,"")
 
 			if self.font is None:
 				raise InitializationError("Could not load font %s" % name)
@@ -62,7 +62,7 @@
 	"""
 	Load fonts from a config file. These are then available via their name.
 	"""
-	from manager import Manager
+	from internal import get_manager
 
 	for font in Font.loadFromFile(filename):
-		Manager.manager.addFont(font)
+		get_manager().addFont(font)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/extensions/pychan/internal.py	Sat Mar 14 12:13:29 2009 +0000
@@ -0,0 +1,208 @@
+# coding: utf-8
+
+from compat import guichan, in_fife
+import widgets
+import fonts
+from exceptions import *
+from traceback import print_exc
+
+def get_manager():
+	"""
+	Get the manager from inside pychan.
+
+	To avoid cyclic imports write::
+	   from internal import get_manager
+	"""
+	return Manager.manager
+
+def screen_width():
+	return get_manager().hook.screen_width
+
+def screen_height():
+	return get_manager().hook.screen_height
+
+class Manager(object):
+	manager = None
+
+	def __init__(self, hook, debug = False):
+		super(Manager,self).__init__()
+		self.hook = hook
+		self.debug = debug
+
+		if in_fife:
+			if not hook.engine.getEventManager():
+				raise InitializationError("No event manager installed.")
+			if not hook.engine.getGuiManager():
+				raise InitializationError("No GUI manager installed.")
+
+		self.fonts = {}
+		#glyphs = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/:();%`\'*#=[]"'
+		self.fonts['default'] = hook.default_font
+
+		self.styles = {}
+		self.addStyle('default',DEFAULT_STYLE)
+
+                Manager.manager = self
+
+		# Setup synchronous dialogs
+		self.mainLoop = None
+		self.breakFromMainLoop = None
+		self.can_execute = False
+
+		# Autopos
+		from autoposition import placeWidget
+		self.placeWidget = placeWidget
+
+	def setupModalExecution(self,mainLoop,breakFromMainLoop):
+		"""
+		Setup synchronous execution of dialogs.
+		"""
+		self.mainLoop = mainLoop
+		self.breakFromMainLoop = breakFromMainLoop
+		self.can_execute = True
+
+	def show(self,widget):
+		"""
+		Shows a widget on screen. Used by L{Widget.show} - do not use directly.
+		"""
+		self.placeWidget(widget, widget.position_technique)
+		self.hook.add_widget( widget.real_widget )
+
+	def hide(self,widget):
+		"""
+		Hides a widget again. Used by L{Widget.hide} - do not use directly.
+		"""
+		self.hook.remove_widget( widget.real_widget )
+
+	def setDefaultFont(self,name):
+		self.fonts['default'] = self.getFont(name)
+
+	def getFont(self,name):
+		"""
+		B{pending deprecation}
+
+		Returns a GuiFont identified by its name.
+
+		@param name: A string identifier from the font definitions in pychans config files.
+		"""
+		if in_fife:
+			font = self.fonts.get(name)
+			if isinstance(font,guichan.GuiFont):
+				return font
+			if hasattr(font,"font") and isinstance(getattr(font,"font"),guichan.GuiFont):
+				return font.font
+			return fonts.parseFontSpec(name)
+		else:
+			return self.hook.get_font(name)
+
+	def addFont(self,font):
+		"""
+		B{deprecated}
+
+		Add a font to the font registry. It's not necessary to call this directly.
+		But it expects a L{Font} instance and throws an L{InitializationError}
+		otherwise.
+
+		@param font: A L{Font} instance.
+		"""
+		if not isinstance(font,fonts.Font):
+			raise InitializationError("PyChan Manager expected a fonts.Font instance, not %s." % repr(font))
+		self.fonts[font.name] = font
+
+	def addStyle(self,name,style):
+		style = self._remapStyleKeys(style)
+
+		for k,v in self.styles.get('default',{}).items():
+			style[k] = style.get(k,v)
+		self.styles[name] = style
+
+	def stylize(self,widget, style, **kwargs):
+		style = self.styles[style]
+		for k,v in style.get('default',{}).items():
+			v = kwargs.get(k,v)
+			setattr(widget,k,v)
+
+		cls = widget.__class__
+		for applicable,specstyle in style.items():
+			if not isinstance(applicable,tuple):
+				applicable = (applicable,)
+			if cls in applicable:
+				for k,v in specstyle.items():
+					v = kwargs.get(k,v)
+					setattr(widget,k,v)
+
+	def _remapStyleKeys(self,style):
+		# Remap class names, create copy:
+		def _toClass(class_):
+			if class_ == "default":
+				return class_
+
+			if type(class_) == type(widgets.Widget) and issubclass(class_,widgets.Widget):
+				return class_
+			if not widgets.WIDGETS.has_key(str(class_)):
+				raise InitializationError("Can't resolve %s to a widget class." % repr(class_))
+			return widgets.WIDGETS[str(class_)]
+
+		style_copy = {}
+		for k,v in style.items():
+			if isinstance(k,tuple):
+				new_k = tuple(map(_toClass,k))
+			else:
+				new_k = _toClass(k)
+			style_copy[new_k] = v
+		return style_copy
+
+	def loadImage(self,filename):
+		if not filename:
+			  raise InitializationError("Empty Image file.")
+		return self.hook.load_image(filename)
+
+# Default Widget style.
+
+DEFAULT_STYLE = {
+	'default' : {
+		'border_size': 0,
+		'margins': (0,0),
+		'base_color' : guichan.Color(28,28,28),
+		'foreground_color' : guichan.Color(255,255,255),
+		'background_color' : guichan.Color(50,50,50),
+	},
+	'Button' : {
+		'border_size': 2,
+		'margins' : (5,2),
+		'min_size' : (15,10),
+	},
+	'CheckBox' : {
+		'border_size': 0,
+	},
+	'RadioButton' : {
+		'border_size': 0,
+		'background_color' : guichan.Color(0,0,0),
+	},
+	'Label' : {
+		'border_size': 0,
+	},
+	'ClickLabel' : {
+		'border_size': 0,
+	},
+	'ListBox' : {
+		'border_size': 0,
+	},
+	'Window' : {
+		'border_size': 0,
+		'margins': (5,5),
+		'opaque' : 1,
+		'titlebar_height' : 12,
+		'vexpanding' : 1,
+		#'background_image' : 'gui/backgrounds/background.png',
+		#'font' : 'samanata_large'
+	},
+	'TextBox' : {
+	},
+	('Container','HBox','VBox') : {
+		'border_size': 0,
+		'margins': (0,0),
+		'padding':2,
+		#'background_image' : 'gui/backgrounds/background.png',
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/extensions/pychan/layout.py	Sat Mar 14 12:13:29 2009 +0000
@@ -0,0 +1,278 @@
+# coding: utf-8
+
+from attrs import IntAttr
+
+AlignTop, AlignBottom, AlignLeft, AlignRight, AlignCenter = range(5)
+def isLayouted(widget):
+	return isinstance(widget,LayoutBase)
+
+class LayoutBase(object):
+	"""
+	This class is at the core of the layout engine. The two MixIn classes L{VBoxLayoutMixin}
+	and L{HBoxLayoutMixin} specialise on this by reimplementing the C{resizeToContent} and
+	the C{expandContent} methods.
+
+	Dynamic Layouting
+	-----------------
+
+	The layout is calculated in the L{Widget.show} method. Thus if you modify the layout,
+	by adding or removing child widgets for example, you have to call L{widgets.Widget.adaptLayout}
+	so that the changes ripple through the widget hierachy.
+
+	Internals
+	---------
+
+	At the core the layout engine works in two passes:
+
+	Before a root widget loaded by the XML code is shown, its resizeToContent method
+	is called recursively (walking the widget containment relation in post order).
+	This shrinks all HBoxes and VBoxes to their minimum heigt and width.
+	After that the expandContent method is called recursively in the same order,
+	which will re-align the widgets if there is space left AND if a Spacer is contained.
+
+	Inside bare Container instances (without a Layout MixIn) absolute positioning
+	can be used.
+	"""
+	def __init__(self,align = (AlignLeft,AlignTop), **kwargs):
+		self.align = align
+		self.spacer = []
+		super(LayoutBase,self).__init__(**kwargs)
+
+	def addSpacer(self,spacer):
+		self.spacer.append(spacer)
+		spacer.index = len(self.children)
+
+	def xdelta(self,widget):return 0
+	def ydelta(self,widget):return 0
+
+	def _applyHeight(self, spacers = []):
+		y = self.border_size + self.margins[1]
+		ydelta = map(self.ydelta,self.children)
+		for index, child in enumerate(self.children):
+			while spacers and spacers[0].index == index:
+				y += spacers.pop(0).size
+			child.y = y
+			y += ydelta.pop(0)
+
+	def _adjustHeightWithSpacer(self):
+		pass
+
+	def _applyWidth(self, spacers = []):
+		x = self.border_size + self.margins[0]
+		xdelta = map(self.xdelta,self.children)
+		for index, child in enumerate(self.children):
+			while spacers and spacers[0].index == index:
+				x += spacers.pop(0).size
+			child.x = x
+			x += xdelta.pop(0)
+
+	def _expandWidthSpacer(self):
+		xdelta = map(self.xdelta,self.children)
+		xdelta += [spacer.min_size for spacer in self.spacer]
+
+		available_space = self.width - 2*self.margins[0] - 2*self.border_size - self._extra_border[0]
+
+		used_space = sum(xdelta)
+		if self.children:
+			used_space -= self.padding
+		if used_space >= available_space:
+			return
+
+		expandable_items = self._getExpanders(vertical=False)
+		#print "AS/US - before",self,[o.width for o in expandable_items]
+		#print "SPACERS",self.spacer
+
+		index = 0
+		while used_space < available_space and expandable_items:
+			index = index % len(expandable_items)
+
+			expander = expandable_items[index]
+			old_width = expander.width
+			expander.width += 1
+			if old_width == expander.width:
+				expandable_items.pop(index)
+			else:
+				used_space += 1
+				index += 1
+
+		#print "AS/US - after",self,[o.width for o in expandable_items]
+		#print "SPACERS",self.spacer
+		self._applyWidth(spacers = self.spacer[:])
+
+	def _expandHeightSpacer(self):
+		ydelta = map(self.ydelta,self.children)
+		ydelta += [spacer.min_size for spacer in self.spacer]
+
+		available_space = self.height - 2*self.margins[1] - 2*self.border_size - self._extra_border[1]
+
+		used_space = sum(ydelta)
+		if self.children:
+			used_space -= self.padding
+
+		if used_space >= available_space:
+			return
+
+		expandable_items = self._getExpanders(vertical=True)
+		#print "AS/US - before",self,[o.height for o in expandable_items]
+
+		index = 0
+		while used_space < available_space and expandable_items:
+			index = index % len(expandable_items)
+
+			expander = expandable_items[index]
+			old_width = expander.height
+			expander.height += 1
+			if old_width == expander.height:
+				expandable_items.pop(index)
+			else:
+				used_space += 1
+				index += 1
+
+		#print "AS/US - after",self,[o.height for o in expandable_items]
+		self._applyHeight(spacers = self.spacer[:])
+
+
+	def _getExpanders(self,vertical=True):
+		expanders = []
+		spacers = self.spacer[:]
+		for index, child in enumerate(self.children):
+			if spacers and spacers[0].index == index:
+				expanders.append( spacers.pop(0) )
+			#print self,child,child.expanding
+			if child.vexpanding and vertical:
+				expanders.append( child )
+			if child.hexpanding and not vertical:
+				expanders.append( child )
+		return expanders + spacers
+
+	def _resetSpacers(self):
+		for spacer in self.spacer:
+			spacer.size = 0
+
+class VBoxLayoutMixin(LayoutBase):
+	"""
+	A mixin class for a vertical layout. Do not use directly.
+	"""
+	def __init__(self,**kwargs):
+		super(VBoxLayoutMixin,self).__init__(**kwargs)
+
+	def resizeToContent(self, recurse = True):
+		self._resetSpacers()
+
+		max_w = self.getMaxChildrenWidth()
+		x = self.margins[0] + self.border_size
+		y = self.margins[1] + self.border_size
+		for widget in self.children:
+			widget.width = max_w
+			y += widget.height + self.padding
+
+		if self.children:
+			y -= self.padding
+
+		y += sum([spacer.min_size for spacer in self.spacer])
+
+		self.height = y + self.margins[1] + self.border_size + self._extra_border[1]
+		self.width = max_w + 2*x + self._extra_border[0]
+
+		self._applyHeight(spacers = self.spacer[:])
+		self._applyWidth()
+
+	def expandContent(self):
+		self._expandHeightSpacer()
+		if not self.hexpanding:return
+		for widget in self.children:
+			widget.width = self.width - 2*self.margins[0] - 2*self.border_size - self._extra_border[0]
+
+
+	def ydelta(self,widget):return widget.height + self.padding
+
+class HBoxLayoutMixin(LayoutBase):
+	"""
+	A mixin class for a horizontal layout. Do not use directly.
+	"""
+	def __init__(self,**kwargs):
+		super(HBoxLayoutMixin,self).__init__(**kwargs)
+
+	def resizeToContent(self, recurse = True):
+		self._resetSpacers()
+
+		max_h = self.getMaxChildrenHeight()
+		x = self.margins[0] + self.border_size
+		y = self.margins[1] + self.border_size
+		for widget in self.children:
+			widget.height = max_h
+			x += widget.width + self.padding
+		if self.children:
+			x -= self.padding
+		x += sum([spacer.min_size for spacer in self.spacer])
+
+		self.width = x + self.margins[0] + self._extra_border[0]
+		self.height = max_h + 2*y + self._extra_border[1]
+
+		self._applyHeight()
+		self._applyWidth(spacers = self.spacer[:])
+
+	def expandContent(self):
+		self._expandWidthSpacer()
+		if not self.vexpanding:return
+		for widget in self.children:
+			widget.height = self.height - 2*self.margins[1] - 2*self.border_size - self._extra_border[1]
+
+	def xdelta(self,widget):return widget.width + self.padding
+
+class Spacer(object):
+	""" A spacer represents expandable or fixed 'whitespace' in the GUI.
+
+	In a XML file you can get this by adding a <Spacer /> inside a VBox or
+	HBox element (Windows implicitly are VBox elements).
+
+	Attributes
+	----------
+
+	As with widgets a number of attributes can be set on a spacer (inside the XML definition).
+	
+	  - min_size: Int: The minimal size this Spacer is allowed to have.
+	  - max_size: Int: The maximal size this Spacer is allowed to have.
+	  - fixed_size: Int: Set min_size and max_size to the same vale - effectively a Fixed size spacer. 
+
+	"""
+
+	ATTRIBUTES = [
+		IntAttr('min_size'), IntAttr('size'), IntAttr('max_size'),
+		IntAttr('fixed_size'),
+	]
+
+	def __init__(self,parent=None,**kwargs):
+		self._parent = parent
+		self.min_size = 0
+		self.max_size = 1000
+		self.size = 0
+
+	def __str__(self):
+		return "Spacer(parent.name='%s')" % getattr(self._parent,'name','None')
+
+	def __repr__(self):
+		return "<Spacer(parent.name='%s') at %x>" % (getattr(self._parent,'name','None'),id(self))
+
+	def _getSize(self):
+		self.size = self._size
+		return self._size
+	def _setSize(self,size):
+		self._size = max(self.min_size, min(self.max_size,size))
+	size = property(_getSize,_setSize)
+
+	# Alias for size
+	width = property(_getSize,_setSize)
+	height = property(_getSize,_setSize)
+
+	def _setFixedSize(self,size):
+		self.min_size = self.max_size = size
+		self.size = size
+	fixed_size = property(fset=_setFixedSize)
+
+	def _isExpanding(self):
+		if self.min_size < self.max_size:
+			return 1
+		return 0
+	vexpanding = property(_isExpanding)
+	hexpanding = property(_isExpanding)
--- a/engine/extensions/pychan/tools.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/extensions/pychan/tools.py	Sat Mar 14 12:13:29 2009 +0000
@@ -1,10 +1,14 @@
 # coding: utf-8
 
+"""
+Functional utilities designed for pychan use cases.
+"""
+
 import exceptions
 
 ### Functools ###
 
-def applyOnlySuitable(func,**kwargs):
+def applyOnlySuitable(func,*args,**kwargs):
 	"""
 	This nifty little function takes another function and applies it to a dictionary of
 	keyword arguments. If the supplied function does not expect one or more of the
@@ -20,11 +24,11 @@
 
 	#http://docs.python.org/lib/inspect-types.html
 	if code.co_flags & 8:
-		return func(**kwargs)
+		return func(*args,**kwargs)
 	for name,value in kwargs.items():
 		if name not in varnames:
 			del kwargs[name]
-	return func(**kwargs)
+	return func(*args,**kwargs)
 
 def callbackWithArguments(callback,*args,**kwargs):
 	"""
@@ -91,13 +95,31 @@
 			do_calls.append(name[4:])
 			del kwargs[name]
 
-	def callback(widget=None):
+	def attrSet_callback(widget=None):
 		for name,value in kwargs.items():
 			setattr(widget,name,value)
 		for method_name in do_calls:
 			method = getattr(widget,method_name)
 			method()
-	return callback
+	return attrSet_callback
+
+def chainCallbacks(*args):
+	"""
+	Chains callbacks to be called one after the other.
+	
+	Example Usage::
+	    def print_event(event=0):
+	      print event
+	    def print_widget(widget=0):
+	      print widget
+            callback = tools.chainCallbacks(doSomethingUseful, print_event, print_widget)
+	    guiElement.capture(callback)
+	"""
+	callbacks = args
+	def chain_callback(event=0,widget=0):
+		for callback in callbacks:
+			applyOnlySuitable(callback, event=event, widget=widget)
+	return chain_callback
 
 def this_is_deprecated(func,message=None):
 	if message is None:
--- a/engine/extensions/pychan/widgets.py	Sat Mar 14 12:03:56 2009 +0000
+++ b/engine/extensions/pychan/widgets.py	Sat Mar 14 12:13:29 2009 +0000
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # coding: utf-8
 ### Widget/Container Base Classes ###
 
@@ -412,6 +413,8 @@
 		for name,data in dataMap.items():
 			widgetList = self.findChildren(name = name)
 			if len(widgetList) != 1:
+				if get_manager().debug:
+					self.listNamedWidgets()
 				raise RuntimeError("DistributeData can only handle widgets with unique names.")
 			widgetList[0].setData(data)
 
@@ -431,6 +434,8 @@
 		for name in widgetNames:
 			widgetList = self.findChildren(name = name)
 			if len(widgetList) != 1:
+				if get_manager().debug:
+					self.listNamedWidgets()
 				raise RuntimeError("CollectData can only handle widgets with unique names.")
 
 			dataMap[name] = widgetList[0].getData()
@@ -461,6 +466,8 @@
 		for name in widgetNames:
 			widgetList = self.findChildren(name = name)
 			if len(widgetList) != 1:
+				if get_manager().debug:
+					self.listNamedWidgets()
 				raise RuntimeError("CollectData can only handle widgets with unique names.")
 			dataList.append( widgetList[0].getData() )
 		if len(dataList) == 1: