Mercurial > fife-parpg
diff engine/extensions/pychan/widgets/containers.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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/widgets/containers.py Thu Mar 26 16:20:16 2009 +0000 @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- + +from common import * +from widget import Widget + +### Containers + Layout code ### + +class Container(Widget): + """ + This is the basic container class. It provides space in which child widgets can + be position via the position attribute. If you want to use the layout engine, + you have to use derived containers with vertical or horizontal orientation + (L{VBox} or L{HBox}) + + New Attributes + ============== + + - padding - Integer: Not used in the Container class istelf, distance between child widgets. + - background_image - Set this to a GuiImage or a resource location (simply a filename). + The image will be tiled over the background area. + - opaque - Boolean: Whether the background should be drawn at all. Set this to False + to make the widget transparent. + - children - Just contains the list of contained child widgets. Do NOT modify. + """ + + ATTRIBUTES = Widget.ATTRIBUTES + [ IntAttr('padding'), Attr('background_image'), BoolAttr('opaque'),PointAttr('margins') ] + + def __init__(self,padding=5,margins=(5,5),_real_widget=None, **kwargs): + self.real_widget = _real_widget or fife.Container() + self.children = [] + self.margins = margins + self.padding = padding + self._background = [] + self._background_image = None + super(Container,self).__init__(**kwargs) + + def addChild(self, widget): + widget.parent = self + self.children.append(widget) + self.real_widget.add(widget.real_widget) + + def removeChild(self,widget): + if not widget in self.children: + raise RuntimeError("%s does not have %s as direct child widget." % (str(self),str(widget))) + self.children.remove(widget) + self.real_widget.remove(widget.real_widget) + widget.parent = None + + def add(self,*widgets): + print "PyChan: Deprecation warning: Please use 'addChild' or 'addChildren' instead." + self.addChildren(*widgets) + + def getMaxChildrenWidth(self): + if not self.children: return 0 + return max(widget.width for widget in self.children) + + def getMaxChildrenHeight(self): + if not self.children: return 0 + return max(widget.height for widget in self.children) + + def deepApply(self,visitorFunc): + for child in self.children: + child.deepApply(visitorFunc) + visitorFunc(self) + + def beforeShow(self): + self._resetTiling() + + def _resetTiling(self): + image = self._background_image + if image is None: + return + + back_w,back_h = self.width, self.height + image_w, image_h = image.getWidth(), image.getHeight() + + map(self.real_widget.remove,self._background) + + # Now tile the background over the widget + self._background = [] + icon = fife.Icon(image) + x, w = 0, image_w + while x < back_w: + y, h = 0, image_h + while y < self.height: + icon = fife.Icon(image) + icon.setPosition(x,y) + self._background.append(icon) + y += h + x += w + map(self.real_widget.add,self._background) + for tile in self._background: + tile.requestMoveToBottom() + + def setBackgroundImage(self,image): + self._background = getattr(self,'_background',None) + if image is None: + self._background_image = image + map(self.real_widget.remove,self._background) + self._background = [] + + # Background generation is done in _resetTiling + + if not isinstance(image, fife.GuiImage): + image = get_manager().loadImage(image) + self._background_image = image + + def getBackgroundImage(self): return self._background_image + background_image = property(getBackgroundImage,setBackgroundImage) + + def _setOpaque(self,opaque): self.real_widget.setOpaque(opaque) + def _getOpaque(self): return self.real_widget.isOpaque() + opaque = property(_getOpaque,_setOpaque) + +AlignTop, AlignBottom, AlignLeft, AlignRight, AlignCenter = range(5) + +class LayoutBase(object): + """ + This class is at the core of the layout engine. The two MixIn classes L{VBoxLayoutMixin} + and L{HBoxLayoutMixin} specialise on this by reimplementing the C{resizeToContent} and + the C{expandContent} methods. + + Dynamic Layouting + ----------------- + + The layout is calculated in the L{Widget.show} method. Thus if you modify the layout, + by adding or removing child widgets for example, you have to call L{Widget.adaptLayout} + so that the changes ripple through the widget hierachy. + + Internals + --------- + + At the core the layout engine works in two passes: + + Before a root widget loaded by the XML code is shown, its resizeToContent method + is called recursively (walking the widget containment relation in post order). + This shrinks all HBoxes and VBoxes to their minimum heigt and width. + After that the expandContent method is called recursively in the same order, + which will re-align the widgets if there is space left AND if a Spacer is contained. + + Inside bare Container instances (without a Layout MixIn) absolute positioning + can be used. + """ + def __init__(self,align = (AlignLeft,AlignTop), **kwargs): + self.align = align + self.spacer = None + super(LayoutBase,self).__init__(**kwargs) + + def addSpacer(self,spacer): + if self.spacer: + raise RuntimeException("Already a Spacer in %s!" % str(self)) + self.spacer = spacer + spacer.index = len(self.children) + + def xdelta(self,widget):return 0 + def ydelta(self,widget):return 0 + + def _adjustHeight(self): + if self.align[1] == AlignTop:return #dy = 0 + if self.align[1] == AlignBottom: + y = self.height - self.childarea[1] - self.border_size - self.margins[1] + else: + y = (self.height - self.childarea[1] - self.border_size - self.margins[1])/2 + for widget in self.children: + widget.y = y + y += self.ydelta(widget) + + def _adjustHeightWithSpacer(self): + pass + + def _adjustWidth(self): + if self.align[0] == AlignLeft:return #dx = 0 + if self.align[0] == AlignRight: + x = self.width - self.childarea[0] - self.border_size - self.margins[0] + else: + x = (self.width - self.childarea[0] - self.border_size - self.margins[0])/2 + for widget in self.children: + widget.x = x + x += self.xdelta(widget) + + def _expandWidthSpacer(self): + x = self.border_size + self.margins[0] + xdelta = map(self.xdelta,self.children) + + for widget in self.children[:self.spacer.index]: + widget.x = x + x += xdelta.pop(0) + + x = self.width - sum(xdelta) - self.border_size - self.margins[0] + for widget in self.children[self.spacer.index:]: + widget.x = x + x += xdelta.pop(0) + + def _expandHeightSpacer(self): + y = self.border_size + self.margins[1] + ydelta = map(self.ydelta,self.children) + + for widget in self.children[:self.spacer.index]: + widget.y = y + y += ydelta.pop(0) + + y = self.height - sum(ydelta) - self.border_size - self.margins[1] + for widget in self.children[self.spacer.index:]: + widget.y = y + y += ydelta.pop(0) + + +class VBoxLayoutMixin(LayoutBase): + """ + A mixin class for a vertical layout. Do not use directly. + """ + def __init__(self,**kwargs): + super(VBoxLayoutMixin,self).__init__(**kwargs) + + def resizeToContent(self, recurse = True): + max_w = self.getMaxChildrenWidth() + x = self.margins[0] + self.border_size + y = self.margins[1] + self.border_size + for widget in self.children: + widget.x = x + widget.y = y + widget.width = max_w + y += widget.height + self.padding + + #Add the padding for the spacer. + if self.spacer: + y += self.padding + + self.height = y + self.margins[1] - self.padding + self.width = max_w + 2*x + self.childarea = max_w, y - self.padding - self.margins[1] + + self._adjustHeight() + self._adjustWidth() + + def expandContent(self): + if self.spacer: + self._expandHeightSpacer() + + def ydelta(self,widget):return widget.height + self.padding + +class HBoxLayoutMixin(LayoutBase): + """ + A mixin class for a horizontal layout. Do not use directly. + """ + def __init__(self,**kwargs): + super(HBoxLayoutMixin,self).__init__(**kwargs) + + def resizeToContent(self, recurse = True): + max_h = self.getMaxChildrenHeight() + x = self.margins[0] + self.border_size + y = self.margins[1] + self.border_size + for widget in self.children: + widget.x = x + widget.y = y + widget.height = max_h + x += widget.width + self.padding + + #Add the padding for the spacer. + if self.spacer: + x += self.padding + + self.width = x + self.margins[0] - self.padding + self.height = max_h + 2*y + self.childarea = x - self.margins[0] - self.padding, max_h + + self._adjustHeight() + self._adjustWidth() + + def expandContent(self): + if self.spacer: + self._expandWidthSpacer() + + def xdelta(self,widget):return widget.width + self.padding + + +class VBox(VBoxLayoutMixin,Container): + """ + A vertically aligned box - for containement of child widgets. + + Widgets added to this container widget, will layout on top of each other. + Also the minimal width of the container will be the maximum of the minimal + widths of the contained widgets. + + The default alignment is to the top. This can be changed by adding a Spacer + to the widget at any point (but only one!). The spacer will expand, so that + widgets above the spacer are aligned to the top, while widgets below the spacer + are aligned to the bottom. + """ + def __init__(self,padding=5,**kwargs): + super(VBox,self).__init__(**kwargs) + self.padding = padding + + +class HBox(HBoxLayoutMixin,Container): + """ + A horizontally aligned box - for containement of child widgets. + + Please see L{VBox} for details - just change the directions :-). + """ + def __init__(self,padding=5,**kwargs): + super(HBox,self).__init__(**kwargs) + self.padding = padding + +class Window(VBoxLayoutMixin,Container): + """ + A L{VBox} with a draggable title bar aka a window + + New Attributes + ============== + + - title: The Caption of the window + - titlebar_height: The height of the window title bar + """ + + ATTRIBUTES = Container.ATTRIBUTES + [ UnicodeAttr('title'), IntAttr('titlebar_height') ] + + def __init__(self,title=u"title",titlebar_height=0,**kwargs): + super(Window,self).__init__(_real_widget = fife.Window(), **kwargs) + if titlebar_height == 0: + titlebar_height = self.real_font.getHeight() + 4 + self.titlebar_height = titlebar_height + self.title = title + + # Override explicit positioning + self.position_technique = "automatic" + + + def _getTitle(self): return gui2text(self.real_widget.getCaption()) + def _setTitle(self,text): self.real_widget.setCaption(text2gui(text)) + title = property(_getTitle,_setTitle) + + def _getTitleBarHeight(self): return self.real_widget.getTitleBarHeight() + def _setTitleBarHeight(self,h): self.real_widget.setTitleBarHeight(h) + titlebar_height = property(_getTitleBarHeight,_setTitleBarHeight) + + # Hackish way of hiding that title bar height in the perceived height. + # Fixes VBox calculation + def _setHeight(self,h): + h = max(self.min_size[1],h) + h = min(self.max_size[1],h) + self.real_widget.setHeight(h + self.titlebar_height) + def _getHeight(self): return self.real_widget.getHeight() - self.titlebar_height + height = property(_getHeight,_setHeight) + + +# Spacer + +class Spacer(object): + """ A spacer represents expandable 'whitespace' in the GUI. + + In a XML file you can get this by adding a <Spacer /> inside a VBox or + HBox element (Windows implicitly are VBox elements). + + The effect is, that elements before the spacer will be left (top) + and elements after the spacer will be right (bottom) aligned. + + There can only be one spacer in VBox (HBox). + """ + def __init__(self,parent=None,**kwargs): + self._parent = parent + + def __str__(self): + return "Spacer(parent.name='%s')" % getattr(self._parent,'name','None') + + def __repr__(self): + return "<Spacer(parent.name='%s') at %x>" % (getattr(self._parent,'name','None'),id(self))