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))