Mercurial > fife-parpg
diff engine/extensions/pychan/__init__.py @ 0:4a0efb7baf70
* Datasets becomes the new trunk and retires after that :-)
author | mvbarracuda@33b003aa-7bff-0310-803a-e67f0ece8222 |
---|---|
date | Sun, 29 Jun 2008 18:44:17 +0000 |
parents | |
children | 9a1529f9625e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/__init__.py Sun Jun 29 18:44:17 2008 +0000 @@ -0,0 +1,408 @@ +# coding: utf-8 + +"""\ +Pythonic Guichan Wrapper - PyChan +================================= + +Pythonic GUI API. + +Features +-------- + - Simpler Interface + - Very Basic XML Format support + - Very Basic Layout Engine + - Pseudo-Synchronous Dialogs. + - Automagic background tiling (WIP) + - Basic Styling support. + - Simple Font Handling + +TODO +---- + - Make setting parent attribute imply containment relation. + - Finalize Widget.execute + - ClickLabel/Label rework. (In progress) + + - Documentation ( Allways not enough :-( ) + - Completion of above features + - Wrap missing widgets: Slider + - Handle Image Fonts + - Move Font config files to XML, too ... + - Add support for fixed size 'Spacers' + - Add messageBox(text) + + - Implement real Menus + - Implement StackWidget + - Then implement TabWidget + + - GridLayout + - Table + +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. + +Problems +-------- + - Reference counting problems again -sigh- + ... and thus possible leaks. + - High amount of code reuse -> Complex code + - Needs at least new style classes and other goodies. + - Missing documentation on: + - Styling + - ScrollArea + - Fonts + +How to use +========== + +At its core you only need a few functions. +After setting up FIFE you need to initalize +pychan. After that you can load a GUI from an +XML file. Please see the documentation of L{loadXML} +for the details of the XML format +:: + import pychan + pychan.init(fifeEngine) + guiElement = pychan.loadXML("contents/gui/myform.xml") + +The resulting guiElement can be shown and hidden with the +obvious L{widgets.Widget.show} and L{widgets.Widget.hide} methods. + +To get a specific widget you have to give it a name in the XML +definition and use that to extract the widget from the returned +GUI element. +:: + okButton = guiElement.findChild(name="okButton") + myInput = guiElement.findChild(name="myInput") + +The data is extracted and set via direct attribute access. +These are using the python property technique to hide +behind the scenes manipulations. Please keep in mind that +the Layout engine and the exact way the widgets are displayed +is somewhat limited. +:: + myInput.text = "Blahblah" + myList.items = ["1","2"] + guiElement.position = (80,90) + +A dialog without an OK button would be futile - so here's how +you hook widget events to function calls. Every widget +has a L{widgets.Widget.capture} method, which will directly call the passed +function when an widget event occurs. As a convenience a +L{widgets.Widget.mapEvents} function will batch the L{widgets.Widget.findChild} and +L{widgets.Widget.capture} calls in an obvious way. +:: + myButton.capture( application.quit ) + guiElement.mapEvents({ + 'okButton' : self.applyAndClose, + 'closeButton': guiElement.hide + }) + +Other important places to look for information: + - L{widgets.Widget} - Attributes explained. + - L{loadXML} - Explain the XML format. + - L{LayoutBase} - Working of the layout engine. + +Initialization, data distribution and collection +================================================ + +Very often a dialogs text fields, labels and listboxes have to be filled with data +after the creation of the dialog. This can be a tiresome process. +After a dialog has executed, B{other} attributes have to be read out again, +this to can be tiresome. PyChan simplifies both processes. But it treats them as three +processes. One is setting the data that will never be read out again - called B{initial data} - +the text of a checkbox or the list of a listBox are good examples. The second is setting the +data that is mutable by the user and may be read out again - for example the state of a checkbox +or the selected index in a list. The third and final process is collection of the user-mutable data:: + guiElement.distributeInitialData({ + 'myListBox' : choices, + 'myLabel' : map.name, + }) + guiElement.distributeData({ + 'myTextField' : map.description + }) + # ... process dialog. + map.description, choice = guiElement.collectData('myListBox','myTextField') + print "You selected:",choice,", good choice!" + +See L{widgets.Widget.distributeData},L{widgets.Widget.distributeInitialData}, +L{widgets.Widget.collectData} and L{widgets.Widget.collectDataAsDict}. + +Styling and font handling +========================= + +Note: These features are B{work in progress} and likely to change. + +A style is a set of rules for matching widgets and a set of attributes +applied to them after creation. The attributes can be any of the given +attributes. Matching happens currently only by the widget class name +itself. + +As an example the following style - written as a python data structure - +will set the border size of all labels to 10:: + style = { + 'ListBox' : { # Matches all listboxes + 'border_size : 10 # After creation call lisbox.border_size = 10 + } + } + +As a convenience you can use the string B{default} to match all widget +classes and thus - for example apply a common font:: + style = { + 'default' : { + 'font' : 'console_small' + } + } + +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:: + import pychan + pychan.init( fifeEngine ) + pychan.loadFonts( "content/fonts/console.fontdef" ) + +The font definition files are in the following format:: + [Font/FIRST_FONT_NAME] + + type: truetype + source: path/to/font.ttf + # The font size in point + size: 30 + + [Font/SECOND_FONT_NAME] + + type: truetype + source: content/fonts/samanata.ttf + size: 8 + + # And so on. + +I hope the example is clear enough ... Other options you can set: + + - color: A list of comma separated integers from 0 to 255. White (255,255,255) by default. + - antialias: Zero or one - enable or disable antialialising. Enabled by default. + - row_spacing: Extra height per row. Default is 0. + - glyph_spacing: Extra space per glyph. Default is 0. B{Currently buggy in the engine!} + + +Widget hierachy +=============== + +Every widget can be contained in another container widget like L{Window}, L{VBox}, +L{HBox}, L{Container} or L{ScrollArea}. Container widgets can contain any number +of widgets. Thus we have a tree like structure of the widgets - which finally makes +up the window or frame that is placed on the screen. + +In PyChan widgets are supposed to be manipulated via the root of this hierachy, +so that the actual layout can be changed in the XML files without hassle. +It can be compared to how HTML works. + +These bits and pieces connect things up:: + - name - A (hopefully) unique name in the widget hierachy + - findChildren - The accessor method to find widgets by name or any other attribute. + - _parent - The parent widget in the widget hierachy + - deepApply - The method used to walk over the widget hierachy. You have to reimplement + this in case you want to provide custom widgets. + +Wrapping machinery +================== + +The wrapping mechanism works be redirecting attribute access to the Widget +derived classes to a C{real_widget} member variable which in turn is an instance +of the SWIG wrapped Guichan widget. + +To ensure the real widget has already been constructed, when the wrapping machinery +is already in use, this has to be the first attribute to set in the constructors. +This leads to a reversed construction sequence as the super classes constructor +has to be invoked I{after} the subclass specific construction has taken place. + +""" + +__all__ = [ + 'loadXML', + 'loadFonts', + 'init', + 'manager' +] + +import fife, pythonize + +from widgets import * +from exceptions import * + +from fonts import loadFonts + +# Text munging befor adding it to TextBoxes + +### Initialisation ### + +manager = None +def init(engine,debug=False): + """ + This has to be called before any other pychan methods can be used. + It sets up a manager object which is available under pychan.manager. + + @param engine: The FIFE engine object. + """ + from manager import Manager + global manager + manager = Manager(engine,debug) + + +# XML Loader + +from xml.sax import saxutils, handler +from traceback import print_exc + +def traced(f): + """ + Simple decorator that prints tracebacks for any exceptions occuring in a + function. Useful to avoid the infamous 'finally pops bad exception' + that shadows the real cause of the error ... + """ + def traced_f(*args,**kwargs): + try: + return f(*args,**kwargs) + except: + print_exc() + raise + return traced_f + +class _GuiLoader(object, handler.ContentHandler): + def __init__(self): + super(_GuiLoader,self).__init__() + self.root = None + self.indent = "" + self.stack = [] + + def _printTag(self,name,attrs): + if not manager.debug: return + attrstrings = map(lambda t: '%s="%s"' % tuple(map(str,t)),attrs.items()) + tag = "<%s " % name + " ".join(attrstrings) + ">" + print self.indent + tag + + def _resolveTag(self,name): + """ Resolve a XML Tag to a PyChan GUI class. """ + cls = WIDGETS.get(name,None) + if cls is None and name == "Spacer": + cls = Spacer + if cls is None: + raise GuiXMLError("Unknown GUI Element: %s" % name) + return cls + + def _setAttr(self,obj,name,value): + if not hasattr(obj.__class__,'ATTRIBUTES'): + raise PyChanException("The registered widget/spacer class %s does not supply an 'ATTRIBUTES'." + % repr(obj)) + try: + for attr in obj.ATTRIBUTES: + if attr.name == name: + attr.set(obj,value) + return + except GuiXMLError, e: + raise GuiXMLError("Error parsing attr '%s'='%s' for '%s': '%s'" % (name,value,obj,e)) + raise GuiXMLError("Unknown GUI Attribute '%s' on '%s'" % (name,repr(obj))) + + def startElement(self, name, attrs): + self._printTag(name,attrs) + cls = self._resolveTag(name) + if issubclass(cls,Widget): + self.stack.append('gui_element') + self._createInstance(cls,name,attrs) + elif cls == Spacer: + self.stack.append('spacer') + self._createSpacer(cls,name,attrs) + else: + self.stack.append('unknown') + self.indent += " "*4 + + def _createInstance(self,cls,name,attrs): + obj = cls(parent=self.root) + for k,v in attrs.items(): + self._setAttr(obj,k,v) + + if self.root: + self.root.addChild( obj ) + self.root = obj + + def _createSpacer(self,cls,name,attrs): + obj = cls(parent=self.root) + if hasattr(self.root,'add'): + self.root.addSpacer(obj) + else: + raise GuiXMLError("A spacer needs to be added to a container widget!") + self.root = obj + + def endElement(self, name): + self.indent = self.indent[:-4] + if manager.debug: print self.indent + "</%s>" % name + if self.stack.pop() in ('gui_element','spacer'): + self.root = self.root._parent or self.root + +def loadXML(filename_or_stream): + """ + Loads a PyChan XML file and generates a widget from it. + + @param filename_or_stream: A filename or a file-like object (for example using StringIO). + + The XML format is very dynamic, in the sense, that the actual allowed tags and attributes + depend on the PyChan code. + + So when a tag C{Button} is encountered, an instance of class Button will be generated, + and added to the parent object. + All attributes will then be parsed and then set in the following way: + + - position,size,min_size,max_size,margins - These are assumed to be comma separated tuples + of integers. + - foreground_color,base_color,background_color - These are assumed to be triples of comma + separated integers. + - opaque,border_size,padding - These are assumed to be simple integers. + + All other attributes are set verbatim as strings on the generated instance. + In case a Widget does not accept an attribute to be set or the attribute can not be parsed + correctly, the function will raise a GuiXMLError. + + In short:: + <VBox> + <Button text="X" min_size="20,20" base_color="255,0,0" border_size="2" /> + </VBox> + + This result in the following code executed:: + + vbox = VBox(parent=None) + button = Button(parent=vbox) + button.text = "X" + button.min_size = (20,20) + button.base_color = (255,0,0) + button.border_size = 2 + vbox.add( button ) + """ + from xml.sax import parse + loader = _GuiLoader() + parse(filename_or_stream,loader) + return loader.root + +def setupModalExecution(mainLoop,breakFromMainLoop): + """ + Setup the synchronous dialog execution feature. + + You can enable synchronous dialog execution by + passing to functions to this function. + + @param mainLoop: Function - This is regarded as the applications + main loop, which should be able to be called recursively. + It should not take no arguments and return the argument + passed to the second function (breakFromMainLoop). + + @param breakFromMainLoop: Function -This function should cause the + first function to finish and return the passed argument. + + With these to functions dialogs can be executed synchronously. + See L{widgets.Widget.execute}. + """ + if not manager: + raise InitializationError("PyChan is not initialized yet.") + manager.setupModalExecution(mainLoop,breakFromMainLoop)