Mercurial > fife-parpg
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) |