diff engine/python/fife/extensions/pychan/widgets/layout.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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/python/fife/extensions/pychan/widgets/layout.py	Mon Jan 11 23:34:52 2010 +0000
@@ -0,0 +1,309 @@
+# -*- coding: utf-8 -*-
+
+# ####################################################################
+#  Copyright (C) 2005-2009 by the FIFE team
+#  http://www.fifengine.de
+#  This file is part of FIFE.
+#
+#  FIFE is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the
+#  Free Software Foundation, Inc.,
+#  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+# ####################################################################
+
+from fife.extensions.pychan.attrs import IntAttr
+
+AlignTop, AlignBottom, AlignLeft, AlignRight, AlignCenter = range(5)
+def isLayouted(widget):
+	return isinstance(widget,LayoutBase)
+
+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{widgets.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 = []
+		super(LayoutBase,self).__init__(**kwargs)
+
+	def addSpacer(self,spacer):
+		self.spacer.append(spacer)
+		spacer.index = len(self.children)
+
+	def xdelta(self,widget):return 0
+	def ydelta(self,widget):return 0
+
+	def _applyHeight(self, spacers = []):
+		y = self.border_size + self.margins[1]
+		ydelta = map(self.ydelta,self.children)
+		for index, child in enumerate(self.children):
+			while spacers and spacers[0].index == index:
+				y += spacers.pop(0).size
+			child.y = y
+			y += ydelta.pop(0)
+
+	def _adjustHeightWithSpacer(self):
+		pass
+
+	def _applyWidth(self, spacers = []):
+		x = self.border_size + self.margins[0]
+		xdelta = map(self.xdelta,self.children)
+		for index, child in enumerate(self.children):
+			while spacers and spacers[0].index == index:
+				x += spacers.pop(0).size
+			child.x = x
+			x += xdelta.pop(0)
+
+	def _expandWidthSpacer(self):
+		xdelta = map(self.xdelta,self.children)
+		xdelta += [spacer.min_size for spacer in self.spacer]
+
+		available_space = self.width - 2*self.margins[0] - 2*self.border_size - self._extra_border[0]
+
+		used_space = sum(xdelta)
+		if self.children:
+			used_space -= self.padding
+		if used_space >= available_space:
+			return
+
+		expandable_items = self._getExpanders(vertical=False)
+		#print "AS/US - before",self,[o.width for o in expandable_items]
+		#print "SPACERS",self.spacer
+
+		index = 0
+		n = len(expandable_items)
+		while used_space < available_space and expandable_items:
+			index = index % n
+			delta = (available_space - used_space) / n
+			if delta == 0:
+				delta = 1
+
+			expander = expandable_items[index]
+			old_width = expander.width
+			expander.width += delta
+			delta = expander.width - old_width
+			if delta == 0:
+				expandable_items.pop(index)
+				n -= 1
+			else:
+				used_space += delta
+				index += 1
+		#print "AS/US - after",self,[o.width for o in expandable_items]
+		#print "SPACERS",self.spacer
+		self._applyWidth(spacers = self.spacer[:])
+
+	def _expandHeightSpacer(self):
+		ydelta = map(self.ydelta,self.children)
+		ydelta += [spacer.min_size for spacer in self.spacer]
+
+		available_space = self.height - 2*self.margins[1] - 2*self.border_size - self._extra_border[1]
+
+		used_space = sum(ydelta)
+		if self.children:
+			used_space -= self.padding
+
+		if used_space >= available_space:
+			return
+
+		expandable_items = self._getExpanders(vertical=True)
+		#print "AS/US - before",self,[o.height for o in expandable_items]
+
+		index = 0
+		n = len(expandable_items)
+		while used_space < available_space and expandable_items:
+			index = index % n
+			delta = (available_space - used_space) / n
+			if delta == 0:
+				delta = 1
+
+			expander = expandable_items[index]
+			old_height = expander.height
+			expander.height += delta
+			delta = expander.height - old_height
+			if delta == 0:
+				expandable_items.pop(index)
+				n -= 1
+			else:
+				used_space += delta
+				index += 1
+
+		#print "AS/US - after",self,[o.height for o in expandable_items]
+		self._applyHeight(spacers = self.spacer[:])
+
+
+	def _getExpanders(self,vertical=True):
+		expanders = []
+		spacers = self.spacer[:]
+		for index, child in enumerate(self.children):
+			if spacers and spacers[0].index == index:
+				expanders.append( spacers.pop(0) )
+			if child.vexpand and vertical:
+				expanders += [child]*child.vexpand
+			if child.hexpand and not vertical:
+				expanders += [child]*child.hexpand
+		return expanders + spacers
+
+	def _resetSpacers(self):
+		for spacer in self.spacer:
+			spacer.size = 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):
+		self._resetSpacers()
+
+		max_w = self.getMaxChildrenWidth()
+		x = self.margins[0] + self.border_size
+		y = self.margins[1] + self.border_size
+		for widget in self.children:
+			widget.width = max_w
+			y += widget.height + self.padding
+
+		if self.children:
+			y -= self.padding
+
+		y += sum([spacer.min_size for spacer in self.spacer])
+
+		self.height = y + self.margins[1] + self.border_size + self._extra_border[1]
+		self.width = max_w + 2*x + self._extra_border[0]
+
+		self._applyHeight(spacers = self.spacer[:])
+		self._applyWidth()
+
+	def expandContent(self):
+		self._expandHeightSpacer()
+		if not self.hexpand and self.parent:return
+		for widget in self.children:
+			widget.width = self.width - 2*self.margins[0] - 2*self.border_size - self._extra_border[0]
+
+
+	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):
+		self._resetSpacers()
+
+		max_h = self.getMaxChildrenHeight()
+		x = self.margins[0] + self.border_size
+		y = self.margins[1] + self.border_size
+		for widget in self.children:
+			widget.height = max_h
+			x += widget.width + self.padding
+		if self.children:
+			x -= self.padding
+		x += sum([spacer.min_size for spacer in self.spacer])
+
+		self.width = x + self.margins[0] + self._extra_border[0]
+		self.height = max_h + 2*y + self._extra_border[1]
+
+		self._applyHeight()
+		self._applyWidth(spacers = self.spacer[:])
+
+	def expandContent(self):
+		self._expandWidthSpacer()
+		if not self.vexpand and self.parent:return
+		for widget in self.children:
+			widget.height = self.height - 2*self.margins[1] - 2*self.border_size - self._extra_border[1]
+
+	def xdelta(self,widget):return widget.width + self.padding
+
+class Spacer(object):
+	""" A spacer represents expandable or fixed '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).
+
+	Attributes
+	----------
+
+	As with widgets a number of attributes can be set on a spacer (inside the XML definition).
+	
+	  - min_size: Int: The minimal size this Spacer is allowed to have.
+	  - max_size: Int: The maximal size this Spacer is allowed to have.
+	  - fixed_size: Int: Set min_size and max_size to the same vale - effectively a Fixed size spacer. 
+
+	"""
+
+	ATTRIBUTES = [
+		IntAttr('min_size'), IntAttr('size'), IntAttr('max_size'),
+		IntAttr('fixed_size'),
+	]
+
+	def __init__(self,parent=None,**kwargs):
+		self.parent = parent
+		self.min_size = 0
+		self.max_size = 1000
+		self.size = 0
+
+	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))
+
+	def _getSize(self):
+		self.size = self._size
+		return self._size
+	def _setSize(self,size):
+		self._size = max(self.min_size, min(self.max_size,size))
+	size = property(_getSize,_setSize)
+
+	# Alias for size
+	width = property(_getSize,_setSize)
+	height = property(_getSize,_setSize)
+
+	def _setFixedSize(self,size):
+		self.min_size = self.max_size = size
+		self.size = size
+	fixed_size = property(fset=_setFixedSize)
+
+	def _isExpanding(self):
+		if self.min_size < self.max_size:
+			return 1
+		return 0
+	vexpand = property(_isExpanding)
+	hexpand = property(_isExpanding)