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