comparison engine/extensions/pychan/widgets/widget.py @ 248:a2d5e2721489

widgets.py split up.
author phoku@33b003aa-7bff-0310-803a-e67f0ece8222
date Thu, 26 Mar 2009 16:20:16 +0000
parents
children 1cc51d145af9
comparison
equal deleted inserted replaced
247:040387b7167f 248:a2d5e2721489
1 # -*- coding: utf-8 -*-
2
3 import fife, pythonize
4 import pychan.tools as tools
5 import pychan.events as events
6 from pychan.exceptions import *
7 from pychan.attrs import Attr,UnicodeAttr, PointAttr,ColorAttr,BoolAttr,IntAttr,FloatAttr
8 from pychan.properties import ColorProperty
9
10 from pychan.widgets.common import *
11
12 class Widget(object):
13 """
14 This is the common widget base class, which provides most of the wrapping
15 functionality.
16
17 Attributes
18 ==========
19
20 Widgets are manipulated (mostly) through attributes - and these can all be set by XML attributes.
21 Derived widgets will have other attributes. Please see their B{New Attributes} sections. The types of the
22 attributes are pretty straightforward, but note that Position and Color attribute types will also accept
23 C{fife.Point} and C{fife.Color} values.
24
25 - name: String: The identification of the widget, most useful if it is unique within a given widget hiarachy.
26 This is used to find widgets by L{mapEvents},L{distributeInitialData},L{distributeData} and L{collectData}.
27 - position: Position: The position relative to the parent widget - or on screen, if this is the root widget.
28 - size: Position: The real size of the widget (including border and margins). Usually you do not need to set this.
29 A notable exception is the L{ScrollArea}.
30 - min_size: Position: The minimal size this widget is allowed to have. This is enforced through the accessor methods
31 of the actual size attribute.
32 - max_size: Position: The maximal size this widget is allowed to have. This is enforced through the accessor methods
33 of the actual size attribute.
34 - base_color: Color
35 - background_color: Color
36 - foreground_color: Color
37 - selection_color: Color
38 - font: String: This should identify a font that was loaded via L{loadFonts} before.
39 - helptext: Unicode: Text which can be used for e.g. tooltips.
40 - border_size: Integer: The size of the border in pixels.
41 - position_technique: This can be either "automatic" or "explicit" - only L{Window} has this set to "automatic" which
42 results in new windows being centered on screen (for now).
43 If it is set to "explicit" the position attribute will not be touched.
44
45 Convenience Attributes
46 ======================
47
48 These attributes are convenience/shorthand versions of above mentioned attributes and assignment will reflect
49 the associated attributes values. E.g. the following is equivalent::
50 # Set X position, leave Y alone
51 widget.x = 10
52 # Same here
53 posi = widget.position
54 widget.position = (10, posi[1])
55
56 Here they are.
57
58 - x: Integer: The horizontal part of the position attribute.
59 - y: Integer: The vertical part of the position attribute.
60 - width: Integer: The horizontal part of the size attribute.
61 - height: Integer: The vertical part of the size attribute.
62
63 """
64
65 ATTRIBUTES = [ Attr('name'), PointAttr('position'),
66 PointAttr('min_size'), PointAttr('size'), PointAttr('max_size'),
67 ColorAttr('base_color'),ColorAttr('background_color'),ColorAttr('foreground_color'),ColorAttr('selection_color'),
68 Attr('style'), Attr('font'),IntAttr('border_size'),Attr('position_technique'),
69 UnicodeAttr('helptext')
70 ]
71
72 DEFAULT_NAME = '__unnamed__'
73
74 HIDE_SHOW_ERROR = """\
75 You can only show/hide the top widget of a hierachy.
76 Use 'addChild' or 'removeChild' to add/remove labels for example.
77 """
78
79 def __init__(self,parent = None, name = DEFAULT_NAME,
80 size = (-1,-1), min_size=(0,0), max_size=(5000,5000),
81 helptext=u"",
82 style = None, **kwargs):
83
84 assert( hasattr(self,'real_widget') )
85 self.event_mapper = events.EventMapper(self)
86 self._visible = False
87
88 # Data distribution & retrieval settings
89 self.accepts_data = False
90 self.accepts_initial_data = False
91
92 self.parent = parent
93
94 # This will also set the _event_id and call real_widget.setActionEventId
95 self.name = name
96
97 self.min_size = min_size
98 self.max_size = max_size
99 self.size = size
100 self.position_technique = "explicit"
101 self.font = 'default'
102
103 # Inherit style
104 if style is None and parent:
105 style = parent.style
106 self.style = style or "default"
107
108 self.helptext = helptext
109 # Not needed as attrib assignment will trigger manager.stylize call
110 #manager.stylize(self,self.style)
111
112 def execute(self,bind):
113 """
114 Execute a dialog synchronously.
115
116 As argument a dictionary mapping widget names to return values
117 is expected. Events from these widgets will cause this function
118 to return with the associated return value.
119
120 This function will not return until such an event occurs.
121 The widget will be shown before execution and hidden afterwards.
122 You can only execute root widgets.
123
124 Note: This feature is not tested well, and the API will probably
125 change. Otherwise have fun::
126 # Okay this a very condensed example :-)
127 return pychan.loadXML("contents/gui/dialog.xml").execute({ 'okButton' : True, 'closeButton' : False })
128
129 """
130 if not get_manager().can_execute:
131 raise RuntimeError("Synchronous execution is not set up!")
132 if self._parent:
133 raise RuntimeError("You can only 'execute' root widgets, not %s!" % str(self))
134
135 for name,returnValue in bind.items():
136 def _quitThisDialog(returnValue = returnValue ):
137 get_manager().breakFromMainLoop( returnValue )
138 self.hide()
139 self.findChild(name=name).capture( _quitThisDialog )
140 self.show()
141 return get_manager().mainLoop()
142
143 def match(self,**kwargs):
144 """
145 Matches the widget against a list of key-value pairs.
146 Only if all keys are attributes and their value is the same it returns True.
147 """
148 for k,v in kwargs.items():
149 if v != getattr(self,k,None):
150 return False
151 return True
152
153 def capture(self, callback, event_name="action", group_name="default"):
154 """
155 Add a callback to be executed when the widget event occurs on this widget.
156
157 The callback must be either a callable or None.
158 The old event handler (if any) will be overridden by the callback.
159 If None is given, the event will be disabled. You can query L{isCaptured}
160 wether this widgets events are currently captured.
161
162 It might be useful to check out L{tools.callbackWithArguments}.
163
164 @param callback: Event callback - may accept keyword arguments event and widget.
165 @paran event_name: The event to capture - may be one of L{events.EVENTS} and defaults to "action"
166 @paran group_name: Event group.
167
168 Event groups are used to have different B{channels} which don't interfere with each other.
169 For derived widgets that need to capture events it's advised to use the group_name 'widget'.
170 The 'default' group is used by default, and should be reserved for the application programmers.
171 """
172 self.event_mapper.capture( event_name, callback, group_name )
173
174 def isCaptured(self):
175 """
176 Check whether this widgets events are captured
177 (a callback is installed) or not.
178 """
179 return bool(self.event_mapper.getCapturedEvents())
180
181 def show(self):
182 """
183 Show the widget and all contained widgets.
184 """
185 if self._parent:
186 raise RuntimeError(Widget.HIDE_SHOW_ERROR)
187 if self._visible: return
188 self.adaptLayout()
189 self.beforeShow()
190 get_manager().show(self)
191 self._visible = True
192
193 def hide(self):
194 """
195 Hide the widget and all contained widgets.
196 """
197 if self._parent:
198 raise RuntimeError(Widget.HIDE_SHOW_ERROR)
199 if not self._visible: return
200
201 get_manager().hide(self)
202
203 self.afterHide()
204 self._visible = False
205
206 def isVisible(self):
207 """
208 Check whether the widget is currently shown,
209 either directly or as part of a container widget.
210 """
211 widget = self
212 while widget._parent:
213 widget = widget._parent
214 return widget._visible
215
216 def adaptLayout(self,recurse=True):
217 """
218 Execute the Layout engine. Automatically called by L{show}.
219 In case you want to relayout a visible widget.
220 This function will automatically perform the layout adaption
221 from the top-most layouted widget.
222
223 To make this clear consider this arrangement::
224 VBox 1
225 - Container
226 - VBox 2
227 - HBox
228 - Label
229
230 If you call adaptLayout on the Label the layout from the VBox 2
231 will get recalculated, while the VBox 1 stays untouched.
232
233 @param recurse Pass False here to force the layout to start from
234 this widget.
235 """
236 widget = self
237 while widget.parent and recurse:
238 if not isLayouted(widget.parent):
239 break
240 widget = widget.parent
241 widget._recursiveResizeToContent()
242 widget._recursiveExpandContent()
243
244 def beforeShow(self):
245 """
246 This method is called just before the widget is shown.
247 You can override this in derived widgets to add finalization
248 behaviour.
249 """
250
251 def afterHide(self):
252 """
253 This method is called just before the widget is hidden.
254 You can override this in derived widgets to add finalization
255 behaviour.
256 """
257
258 def findChildren(self,**kwargs):
259 """
260 Find all contained child widgets by attribute values.
261
262 Usage::
263 closeButtons = root_widget.findChildren(name='close')
264 buttons = root_widget.findChildren(__class__=pychan.widgets.Button)
265 """
266
267 children = []
268 def _childCollector(widget):
269 if widget.match(**kwargs):
270 children.append(widget)
271 self.deepApply(_childCollector)
272 return children
273
274 def findChild(self,**kwargs):
275 """ Find the first contained child widgets by attribute values.
276
277 Usage::
278 closeButton = root_widget.findChild(name='close')
279 """
280 children = self.findChildren(**kwargs)
281 if children:
282 return children[0]
283 return None
284
285 def addChild(self,widget):
286 """
287 This function adds a widget as child widget and is only implemented
288 in container widgets.
289
290 You'll need to call L{adaptLayout} if the container is already shown,
291 to adapt the layout to the new widget. This doesn't happen
292 automatically.
293 """
294 raise RuntimeError("Trying to add a widget to %s, which doesn't allow this." % repr(self))
295
296 def addChildren(self,*widgets):
297 """
298 Add multiple widgets as children.
299 Only implemented for container widgets. See also L{addChild}
300
301 Usage::
302 container.addChildren( widget1, widget2, ... )
303 # or you can use this on a list
304 container.addChildren( [widget1,widget2,...] )
305 """
306 if len(widgets) == 1 and not isinstance(widgets[0],Widget):
307 widgets = widgets[0]
308 for widget in widgets:
309 self.addChild(widget)
310
311 def removeChild(self,widget):
312 """
313 This function removes a direct child widget and is only implemented
314 in container widgets.
315
316 You'll need to call L{adaptLayout} if the container is already shown,
317 to adapt the layout to the removed widget. This doesn't happen
318 automatically.
319 """
320 raise RuntimeError("Trying to remove a widget from %s, which is not a container widget." % repr(self))
321
322 def removeChildren(self,*widgets):
323 """
324 Remove a list of direct child widgets.
325 All widgets have to be direct child widgets.
326 To 'clear' a container take a look at L{removeAllChildren}.
327 See also L{removeChild}.
328
329 Usage::
330 container.removeChildren( widget1, widget2, ... )
331 # or you can use this on a list
332 container.removeChildren( [widget1,widget2,...] )
333 """
334 if len(widgets) == 1 and not isinstance(widgets[0],Widget):
335 widgets = widgets[0]
336 for widget in widgets:
337 self.removeChild(widget)
338
339 def removeAllChildren(self):
340 """
341 This function will remove all direct child widgets.
342 This will work even for non-container widgets.
343 """
344 children = self.findChildren(parent=self)
345 for widget in children:
346 self.removeChild(widget)
347
348 def mapEvents(self,eventMap,ignoreMissing = False):
349 """
350 Convenience function to map widget events to functions
351 in a batch.
352
353 Subsequent calls of mapEvents will merge events with different
354 widget names and override the previously set callback.
355 You can also pass C{None} instead of a callback, which will
356 disable the event completely.
357
358 @param eventMap: A dictionary with widget/event names as keys and callbacks as values.
359 @param ignoreMissing: Normally this method raises an RuntimeError, when a widget
360 can not be found - this behaviour can be overriden by passing True here.
361
362 The keys in the dictionary are parsed as C{"widgetName/eventName"} with the slash
363 separating the two. If no slash is found the eventName is assumed to be "action".
364
365 Additionally you can supply a group name or channel C{"widgetName/eventName/groupName"}.
366 Event handlers from one group are not overridden by handlers from another group.
367 The default group name is C{"default"}.
368
369 Example::
370 guiElement.mapEvents({
371 "button" : guiElement.hide,
372 "button/mouseEntered" : toggleButtonColorGreen,
373 "button/mouseExited" : toggleButtonColorBlue,
374 })
375
376 """
377 for descr,func in eventMap.items():
378 name, event_name, group_name = events.splitEventDescriptor(descr)
379 #print name, event_name, group_name
380 widget = self.findChild(name=name)
381 if widget:
382 widget.capture( func, event_name = event_name, group_name = group_name )
383 elif not ignoreMissing:
384 raise RuntimeError("No widget with the name: %s" % name)
385
386 def setInitialData(self,data):
387 """
388 Set the initial data on a widget, what this means depends on the Widget.
389 In case the widget does not accept initial data, a L{RuntimeError} is thrown.
390 """
391 if not self.accepts_initial_data:
392 raise RuntimeError("Trying to set data on a widget that does not accept initial data. Widget: %s Data: %s " % (repr(self),repr(data)))
393 self._realSetInitialData(data)
394
395 def setData(self,data):
396 """
397 Set the user-mutable data on a widget, what this means depends on the Widget.
398 In case the widget does not accept data, a L{RuntimeError} is thrown.
399 This is inverse to L{getData}.
400 """
401 if not self.accepts_data:
402 raise RuntimeError("Trying to set data on a widget that does not accept data.")
403 self._realSetData(data)
404
405 def getData(self):
406 """
407 Get the user-mutable data of a widget, what this means depends on the Widget.
408 In case the widget does not have user mutable data, a L{RuntimeError} is thrown.
409 This is inverse to L{setData}.
410 """
411 if not self.accepts_data:
412 raise RuntimeError("Trying to retrieve data from a widget that does not accept data.")
413 return self._realGetData()
414
415 def distributeInitialData(self,initialDataMap):
416 """
417 Distribute B{initial} (not mutable by the user) data from a dictionary over the widgets in the hierachy
418 using the keys as names and the values as the data (which is set via L{setInitialData}).
419 If more than one widget matches - the data is set on ALL matching widgets.
420 By default a missing widget is just ignored.
421
422 Use it like this::
423 guiElement.distributeInitialData({
424 'myTextField' : 'Hello World!',
425 'myListBox' : ["1","2","3"]
426 })
427
428 """
429 for name,data in initialDataMap.items():
430 widgetList = self.findChildren(name = name)
431 for widget in widgetList:
432 widget.setInitialData(data)
433
434 def distributeData(self,dataMap):
435 """
436 Distribute data from a dictionary over the widgets in the hierachy
437 using the keys as names and the values as the data (which is set via L{setData}).
438 This will only accept unique matches.
439
440 Use it like this::
441 guiElement.distributeData({
442 'myTextField' : 'Hello World!',
443 'myListBox' : ["1","2","3"]
444 })
445
446 """
447 for name,data in dataMap.items():
448 widgetList = self.findChildren(name = name)
449 if len(widgetList) != 1:
450 if get_manager().debug:
451 self.listNamedWidgets()
452 raise RuntimeError("DistributeData can only handle widgets with unique names.")
453 widgetList[0].setData(data)
454
455 def collectDataAsDict(self,widgetNames):
456 """
457 Collect data from a widget hierachy by names into a dictionary.
458 This can only handle UNIQUE widget names (in the hierachy)
459 and will raise a RuntimeError if the number of matching widgets
460 is not equal to one.
461
462 Usage::
463 data = guiElement.collectDataAsDict(['myTextField','myListBox'])
464 print "You entered:",data['myTextField']," and selected ",data['myListBox']
465
466 """
467 dataMap = {}
468 for name in widgetNames:
469 widgetList = self.findChildren(name = name)
470 if len(widgetList) != 1:
471 if get_manager().debug:
472 self.listNamedWidgets()
473 raise RuntimeError("CollectData can only handle widgets with unique names.")
474
475 dataMap[name] = widgetList[0].getData()
476 return dataMap
477
478 def collectData(self,*widgetNames):
479 """
480 Collect data from a widget hierachy by names.
481 This can only handle UNIQUE widget names (in the hierachy)
482 and will raise a RuntimeError if the number of matching widgets
483 is not equal to one.
484
485 This function takes an arbitrary number of widget names and
486 returns a list of the collected data in the same order.
487
488 In case only one argument is given, it will return just the
489 data, with out putting it into a list.
490
491 Usage::
492 # Multiple element extraction:
493 text, selected = guiElement.collectData('myTextField','myListBox')
494 print "You entered:",text," and selected item nr",selected
495 # Single elements are handled gracefully, too:
496 test = guiElement.collectData('testElement')
497
498 """
499 dataList = []
500 for name in widgetNames:
501 widgetList = self.findChildren(name = name)
502 if len(widgetList) != 1:
503 if get_manager().debug:
504 self.listNamedWidgets()
505 raise RuntimeError("CollectData can only handle widgets with unique names.")
506 dataList.append( widgetList[0].getData() )
507 if len(dataList) == 1:
508 return dataList[0]
509 return dataList
510
511 def listNamedWidgets(self):
512 """
513 This function will print a list of all currently named child-widgets
514 to the standard output. This is useful for debugging purposes.
515 """
516 def _printNamedWidget(widget):
517 if widget.name != Widget.DEFAULT_NAME:
518 print widget.name.ljust(20),repr(widget).ljust(50),repr(widget._parent)
519 print "Named child widgets of ",repr(self)
520 print "name".ljust(20),"widget".ljust(50),"parent"
521 self.deepApply(_printNamedWidget)
522
523 def stylize(self,style,**kwargs):
524 """
525 Recursively apply a style to all widgets.
526 """
527 def _restyle(widget):
528 get_manager().stylize(widget,style,**kwargs)
529 self.deepApply(_restyle)
530
531 def resizeToContent(self,recurse = True):
532 """
533 Try to shrink the widget, so that it fits closely around its content.
534 Do not call directly.
535 """
536
537 def expandContent(self,recurse = True):
538 """
539 Try to expand any spacer in the widget within the current size.
540 Do not call directly.
541 """
542
543
544 def _recursiveResizeToContent(self):
545 """
546 Recursively call L{resizeToContent}. Uses L{deepApply}.
547 Do not call directly.
548 """
549 def _callResizeToContent(widget):
550 #print "RTC:",widget
551 widget.resizeToContent()
552 self.deepApply(_callResizeToContent)
553
554 def _recursiveExpandContent(self):
555 """
556 Recursively call L{expandContent}. Uses L{deepApply}.
557 Do not call directly.
558 """
559 def _callExpandContent(widget):
560 #print "ETC:",widget
561 widget.expandContent()
562 self.deepApply(_callExpandContent)
563
564 def deepApply(self,visitorFunc):
565 """
566 Recursively apply a callable to all contained widgets and then the widget itself.
567 """
568 visitorFunc(self)
569
570 def sizeChanged(self):
571 pass
572
573 def __str__(self):
574 return "%s(name='%s')" % (self.__class__.__name__,self.name)
575
576 def __repr__(self):
577 return "<%s(name='%s') at %x>" % (self.__class__.__name__,self.name,id(self))
578
579 def _setSize(self,size):
580 if isinstance(size,fife.Point):
581 self.width, self.height = size.x, size.y
582 else:
583 self.width, self.height = size
584
585 def _getSize(self):
586 return self.width, self.height
587
588 def _setPosition(self,size):
589 if isinstance(size,fife.Point):
590 self.x, self.y = size.x, size.y
591 else:
592 self.x, self.y = size
593
594 def _getPosition(self):
595 return self.x, self.y
596
597 def _setX(self,x):self.real_widget.setX(x)
598 def _getX(self): return self.real_widget.getX()
599 def _setY(self,y): self.real_widget.setY(y)
600 def _getY(self): return self.real_widget.getY()
601
602 def _setWidth(self,w):
603 old_width = self.width
604 w = max(self.min_size[0],w)
605 w = min(self.max_size[0],w)
606 self.real_widget.setWidth(w)
607 if w != old_width:
608 self.sizeChanged()
609
610 def _getWidth(self): return self.real_widget.getWidth()
611 def _setHeight(self,h):
612 old_height = self.height
613 h = max(self.min_size[1],h)
614 h = min(self.max_size[1],h)
615 self.real_widget.setHeight(h)
616 if h != old_height:
617 self.sizeChanged()
618
619 def _getHeight(self): return self.real_widget.getHeight()
620
621 def _setFont(self, font):
622 self._font = font
623 self.real_font = get_manager().getFont(font)
624 self.real_widget.setFont(self.real_font)
625 def _getFont(self):
626 return self._font
627
628 def _getBorderSize(self): return self.real_widget.getFrameSize()
629 def _setBorderSize(self,size): self.real_widget.setFrameSize(size)
630
631 base_color = ColorProperty("BaseColor")
632 background_color = ColorProperty("BackgroundColor")
633 foreground_color = ColorProperty("ForegroundColor")
634 selection_color = ColorProperty("SelectionColor")
635
636 def _getStyle(self): return self._style
637 def _setStyle(self,style):
638 self._style = style
639 get_manager().stylize(self,style)
640 style = property(_getStyle,_setStyle)
641
642 def _getParent(self): return self._parent
643 def _setParent(self,parent):
644 self._parent = parent
645 parent = property(_getParent,_setParent)
646
647 def _setName(self,name): self._name = name
648 def _getName(self): return self._name
649 name = property(_getName,_setName)
650
651 x = property(_getX,_setX)
652 y = property(_getY,_setY)
653 width = property(_getWidth,_setWidth)
654 height = property(_getHeight,_setHeight)
655 size = property(_getSize,_setSize)
656 position = property(_getPosition,_setPosition)
657 font = property(_getFont,_setFont)
658 border_size = property(_getBorderSize,_setBorderSize)
659
660 def setEnterCallback(self, cb):
661 """
662 *DEPRECATED*
663
664 Callback is called when mouse enters the area of Widget
665 callback should have form of function(button)
666 """
667 if cb is None:
668 self.capture(None, event_name = "mouseEntered" )
669 return
670
671 def callback(widget=None):
672 return cb(widget)
673 print "PyChan: You are using the DEPRECATED functionality: setEnterCallback."
674 self.capture(callback, event_name = "mouseEntered" )
675
676 def setExitCallback(self, cb):
677 """
678 *DEPRECATED*
679
680 Callback is called when mouse exits the area of Widget
681 callback should have form of function(button)
682 """
683 if cb is None:
684 self.capture(None, event_name = "mouseExited" )
685 return
686
687 def callback(widget=None):
688 return cb(widget)
689 print "PyChan: You are using the DEPRECATED functionality: setExitCallback."
690 self.capture(callback, event_name = "mouseExited" )
691