comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4a0efb7baf70
1 # coding: utf-8
2
3 """\
4 Pythonic Guichan Wrapper - PyChan
5 =================================
6
7 Pythonic GUI API.
8
9 Features
10 --------
11 - Simpler Interface
12 - Very Basic XML Format support
13 - Very Basic Layout Engine
14 - Pseudo-Synchronous Dialogs.
15 - Automagic background tiling (WIP)
16 - Basic Styling support.
17 - Simple Font Handling
18
19 TODO
20 ----
21 - Make setting parent attribute imply containment relation.
22 - Finalize Widget.execute
23 - ClickLabel/Label rework. (In progress)
24
25 - Documentation ( Allways not enough :-( )
26 - Completion of above features
27 - Wrap missing widgets: Slider
28 - Handle Image Fonts
29 - Move Font config files to XML, too ...
30 - Add support for fixed size 'Spacers'
31 - Add messageBox(text)
32
33 - Implement real Menus
34 - Implement StackWidget
35 - Then implement TabWidget
36
37 - GridLayout
38 - Table
39
40 BUGS
41 ----
42 - Focus problems with Widget.execute.
43 - Layout Bugs where max_size of a parent widget get's ignored.
44 - Font.glyph_spacing is rendered incorrectly.
45 - It just looks ugly.
46
47 Problems
48 --------
49 - Reference counting problems again -sigh-
50 ... and thus possible leaks.
51 - High amount of code reuse -> Complex code
52 - Needs at least new style classes and other goodies.
53 - Missing documentation on:
54 - Styling
55 - ScrollArea
56 - Fonts
57
58 How to use
59 ==========
60
61 At its core you only need a few functions.
62 After setting up FIFE you need to initalize
63 pychan. After that you can load a GUI from an
64 XML file. Please see the documentation of L{loadXML}
65 for the details of the XML format
66 ::
67 import pychan
68 pychan.init(fifeEngine)
69 guiElement = pychan.loadXML("contents/gui/myform.xml")
70
71 The resulting guiElement can be shown and hidden with the
72 obvious L{widgets.Widget.show} and L{widgets.Widget.hide} methods.
73
74 To get a specific widget you have to give it a name in the XML
75 definition and use that to extract the widget from the returned
76 GUI element.
77 ::
78 okButton = guiElement.findChild(name="okButton")
79 myInput = guiElement.findChild(name="myInput")
80
81 The data is extracted and set via direct attribute access.
82 These are using the python property technique to hide
83 behind the scenes manipulations. Please keep in mind that
84 the Layout engine and the exact way the widgets are displayed
85 is somewhat limited.
86 ::
87 myInput.text = "Blahblah"
88 myList.items = ["1","2"]
89 guiElement.position = (80,90)
90
91 A dialog without an OK button would be futile - so here's how
92 you hook widget events to function calls. Every widget
93 has a L{widgets.Widget.capture} method, which will directly call the passed
94 function when an widget event occurs. As a convenience a
95 L{widgets.Widget.mapEvents} function will batch the L{widgets.Widget.findChild} and
96 L{widgets.Widget.capture} calls in an obvious way.
97 ::
98 myButton.capture( application.quit )
99 guiElement.mapEvents({
100 'okButton' : self.applyAndClose,
101 'closeButton': guiElement.hide
102 })
103
104 Other important places to look for information:
105 - L{widgets.Widget} - Attributes explained.
106 - L{loadXML} - Explain the XML format.
107 - L{LayoutBase} - Working of the layout engine.
108
109 Initialization, data distribution and collection
110 ================================================
111
112 Very often a dialogs text fields, labels and listboxes have to be filled with data
113 after the creation of the dialog. This can be a tiresome process.
114 After a dialog has executed, B{other} attributes have to be read out again,
115 this to can be tiresome. PyChan simplifies both processes. But it treats them as three
116 processes. One is setting the data that will never be read out again - called B{initial data} -
117 the text of a checkbox or the list of a listBox are good examples. The second is setting the
118 data that is mutable by the user and may be read out again - for example the state of a checkbox
119 or the selected index in a list. The third and final process is collection of the user-mutable data::
120 guiElement.distributeInitialData({
121 'myListBox' : choices,
122 'myLabel' : map.name,
123 })
124 guiElement.distributeData({
125 'myTextField' : map.description
126 })
127 # ... process dialog.
128 map.description, choice = guiElement.collectData('myListBox','myTextField')
129 print "You selected:",choice,", good choice!"
130
131 See L{widgets.Widget.distributeData},L{widgets.Widget.distributeInitialData},
132 L{widgets.Widget.collectData} and L{widgets.Widget.collectDataAsDict}.
133
134 Styling and font handling
135 =========================
136
137 Note: These features are B{work in progress} and likely to change.
138
139 A style is a set of rules for matching widgets and a set of attributes
140 applied to them after creation. The attributes can be any of the given
141 attributes. Matching happens currently only by the widget class name
142 itself.
143
144 As an example the following style - written as a python data structure -
145 will set the border size of all labels to 10::
146 style = {
147 'ListBox' : { # Matches all listboxes
148 'border_size : 10 # After creation call lisbox.border_size = 10
149 }
150 }
151
152 As a convenience you can use the string B{default} to match all widget
153 classes and thus - for example apply a common font::
154 style = {
155 'default' : {
156 'font' : 'console_small'
157 }
158 }
159
160 The font is set via a string identifier pulled from a font definition
161 in a PyChan configuration file. You have to load these by calling
162 L{loadFonts} in your startup code::
163 import pychan
164 pychan.init( fifeEngine )
165 pychan.loadFonts( "content/fonts/console.fontdef" )
166
167 The font definition files are in the following format::
168 [Font/FIRST_FONT_NAME]
169
170 type: truetype
171 source: path/to/font.ttf
172 # The font size in point
173 size: 30
174
175 [Font/SECOND_FONT_NAME]
176
177 type: truetype
178 source: content/fonts/samanata.ttf
179 size: 8
180
181 # And so on.
182
183 I hope the example is clear enough ... Other options you can set:
184
185 - color: A list of comma separated integers from 0 to 255. White (255,255,255) by default.
186 - antialias: Zero or one - enable or disable antialialising. Enabled by default.
187 - row_spacing: Extra height per row. Default is 0.
188 - glyph_spacing: Extra space per glyph. Default is 0. B{Currently buggy in the engine!}
189
190
191 Widget hierachy
192 ===============
193
194 Every widget can be contained in another container widget like L{Window}, L{VBox},
195 L{HBox}, L{Container} or L{ScrollArea}. Container widgets can contain any number
196 of widgets. Thus we have a tree like structure of the widgets - which finally makes
197 up the window or frame that is placed on the screen.
198
199 In PyChan widgets are supposed to be manipulated via the root of this hierachy,
200 so that the actual layout can be changed in the XML files without hassle.
201 It can be compared to how HTML works.
202
203 These bits and pieces connect things up::
204 - name - A (hopefully) unique name in the widget hierachy
205 - findChildren - The accessor method to find widgets by name or any other attribute.
206 - _parent - The parent widget in the widget hierachy
207 - deepApply - The method used to walk over the widget hierachy. You have to reimplement
208 this in case you want to provide custom widgets.
209
210 Wrapping machinery
211 ==================
212
213 The wrapping mechanism works be redirecting attribute access to the Widget
214 derived classes to a C{real_widget} member variable which in turn is an instance
215 of the SWIG wrapped Guichan widget.
216
217 To ensure the real widget has already been constructed, when the wrapping machinery
218 is already in use, this has to be the first attribute to set in the constructors.
219 This leads to a reversed construction sequence as the super classes constructor
220 has to be invoked I{after} the subclass specific construction has taken place.
221
222 """
223
224 __all__ = [
225 'loadXML',
226 'loadFonts',
227 'init',
228 'manager'
229 ]
230
231 import fife, pythonize
232
233 from widgets import *
234 from exceptions import *
235
236 from fonts import loadFonts
237
238 # Text munging befor adding it to TextBoxes
239
240 ### Initialisation ###
241
242 manager = None
243 def init(engine,debug=False):
244 """
245 This has to be called before any other pychan methods can be used.
246 It sets up a manager object which is available under pychan.manager.
247
248 @param engine: The FIFE engine object.
249 """
250 from manager import Manager
251 global manager
252 manager = Manager(engine,debug)
253
254
255 # XML Loader
256
257 from xml.sax import saxutils, handler
258 from traceback import print_exc
259
260 def traced(f):
261 """
262 Simple decorator that prints tracebacks for any exceptions occuring in a
263 function. Useful to avoid the infamous 'finally pops bad exception'
264 that shadows the real cause of the error ...
265 """
266 def traced_f(*args,**kwargs):
267 try:
268 return f(*args,**kwargs)
269 except:
270 print_exc()
271 raise
272 return traced_f
273
274 class _GuiLoader(object, handler.ContentHandler):
275 def __init__(self):
276 super(_GuiLoader,self).__init__()
277 self.root = None
278 self.indent = ""
279 self.stack = []
280
281 def _printTag(self,name,attrs):
282 if not manager.debug: return
283 attrstrings = map(lambda t: '%s="%s"' % tuple(map(str,t)),attrs.items())
284 tag = "<%s " % name + " ".join(attrstrings) + ">"
285 print self.indent + tag
286
287 def _resolveTag(self,name):
288 """ Resolve a XML Tag to a PyChan GUI class. """
289 cls = WIDGETS.get(name,None)
290 if cls is None and name == "Spacer":
291 cls = Spacer
292 if cls is None:
293 raise GuiXMLError("Unknown GUI Element: %s" % name)
294 return cls
295
296 def _setAttr(self,obj,name,value):
297 if not hasattr(obj.__class__,'ATTRIBUTES'):
298 raise PyChanException("The registered widget/spacer class %s does not supply an 'ATTRIBUTES'."
299 % repr(obj))
300 try:
301 for attr in obj.ATTRIBUTES:
302 if attr.name == name:
303 attr.set(obj,value)
304 return
305 except GuiXMLError, e:
306 raise GuiXMLError("Error parsing attr '%s'='%s' for '%s': '%s'" % (name,value,obj,e))
307 raise GuiXMLError("Unknown GUI Attribute '%s' on '%s'" % (name,repr(obj)))
308
309 def startElement(self, name, attrs):
310 self._printTag(name,attrs)
311 cls = self._resolveTag(name)
312 if issubclass(cls,Widget):
313 self.stack.append('gui_element')
314 self._createInstance(cls,name,attrs)
315 elif cls == Spacer:
316 self.stack.append('spacer')
317 self._createSpacer(cls,name,attrs)
318 else:
319 self.stack.append('unknown')
320 self.indent += " "*4
321
322 def _createInstance(self,cls,name,attrs):
323 obj = cls(parent=self.root)
324 for k,v in attrs.items():
325 self._setAttr(obj,k,v)
326
327 if self.root:
328 self.root.addChild( obj )
329 self.root = obj
330
331 def _createSpacer(self,cls,name,attrs):
332 obj = cls(parent=self.root)
333 if hasattr(self.root,'add'):
334 self.root.addSpacer(obj)
335 else:
336 raise GuiXMLError("A spacer needs to be added to a container widget!")
337 self.root = obj
338
339 def endElement(self, name):
340 self.indent = self.indent[:-4]
341 if manager.debug: print self.indent + "</%s>" % name
342 if self.stack.pop() in ('gui_element','spacer'):
343 self.root = self.root._parent or self.root
344
345 def loadXML(filename_or_stream):
346 """
347 Loads a PyChan XML file and generates a widget from it.
348
349 @param filename_or_stream: A filename or a file-like object (for example using StringIO).
350
351 The XML format is very dynamic, in the sense, that the actual allowed tags and attributes
352 depend on the PyChan code.
353
354 So when a tag C{Button} is encountered, an instance of class Button will be generated,
355 and added to the parent object.
356 All attributes will then be parsed and then set in the following way:
357
358 - position,size,min_size,max_size,margins - These are assumed to be comma separated tuples
359 of integers.
360 - foreground_color,base_color,background_color - These are assumed to be triples of comma
361 separated integers.
362 - opaque,border_size,padding - These are assumed to be simple integers.
363
364 All other attributes are set verbatim as strings on the generated instance.
365 In case a Widget does not accept an attribute to be set or the attribute can not be parsed
366 correctly, the function will raise a GuiXMLError.
367
368 In short::
369 <VBox>
370 <Button text="X" min_size="20,20" base_color="255,0,0" border_size="2" />
371 </VBox>
372
373 This result in the following code executed::
374
375 vbox = VBox(parent=None)
376 button = Button(parent=vbox)
377 button.text = "X"
378 button.min_size = (20,20)
379 button.base_color = (255,0,0)
380 button.border_size = 2
381 vbox.add( button )
382 """
383 from xml.sax import parse
384 loader = _GuiLoader()
385 parse(filename_or_stream,loader)
386 return loader.root
387
388 def setupModalExecution(mainLoop,breakFromMainLoop):
389 """
390 Setup the synchronous dialog execution feature.
391
392 You can enable synchronous dialog execution by
393 passing to functions to this function.
394
395 @param mainLoop: Function - This is regarded as the applications
396 main loop, which should be able to be called recursively.
397 It should not take no arguments and return the argument
398 passed to the second function (breakFromMainLoop).
399
400 @param breakFromMainLoop: Function -This function should cause the
401 first function to finish and return the passed argument.
402
403 With these to functions dialogs can be executed synchronously.
404 See L{widgets.Widget.execute}.
405 """
406 if not manager:
407 raise InitializationError("PyChan is not initialized yet.")
408 manager.setupModalExecution(mainLoop,breakFromMainLoop)