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