diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/engine/extensions/pychan/widgets/widget.py	Thu Mar 26 16:20:16 2009 +0000
@@ -0,0 +1,691 @@
+# -*- coding: utf-8 -*-
+
+import fife, pythonize
+import pychan.tools as tools
+import pychan.events as events
+from pychan.exceptions import *
+from pychan.attrs import Attr,UnicodeAttr, PointAttr,ColorAttr,BoolAttr,IntAttr,FloatAttr
+from pychan.properties import ColorProperty
+
+from pychan.widgets.common import *
+
+class Widget(object):
+	"""
+	This is the common widget base class, which provides most of the wrapping
+	functionality.
+
+	Attributes
+	==========
+
+	Widgets are manipulated (mostly) through attributes - and these can all be set by XML attributes.
+	Derived widgets will have other attributes. Please see their B{New Attributes} sections. The types of the
+	attributes are pretty straightforward, but note that Position and Color attribute types will also accept
+	C{fife.Point} and C{fife.Color} values.
+
+	  - name: String: The identification of the widget, most useful if it is unique within a given widget hiarachy.
+	  This is used to find widgets by L{mapEvents},L{distributeInitialData},L{distributeData} and L{collectData}.
+	  - position: Position: The position relative to the parent widget - or on screen, if this is the root widget.
+	  - size: Position: The real size of the widget (including border and margins). Usually you do not need to set this.
+	  A notable exception is the L{ScrollArea}.
+	  - min_size: Position: The minimal size this widget is allowed to have. This is enforced through the accessor methods
+	  of the actual size attribute.
+	  - max_size: Position: The maximal size this widget is allowed to have. This is enforced through the accessor methods
+	  of the actual size attribute.
+	  - base_color: Color
+	  - background_color: Color
+	  - foreground_color: Color
+	  - selection_color: Color
+	  - font: String: This should identify a font that was loaded via L{loadFonts} before.
+	  - helptext: Unicode: Text which can be used for e.g. tooltips.
+	  - border_size: Integer: The size of the border in pixels.
+	  - position_technique: This can be either "automatic" or "explicit" - only L{Window} has this set to "automatic" which
+	  results in new windows being centered on screen (for now).
+	  If it is set to "explicit" the position attribute will not be touched.
+
+	Convenience Attributes
+	======================
+
+	These attributes are convenience/shorthand versions of above mentioned attributes and assignment will reflect
+	the associated attributes values. E.g. the following is equivalent::
+	   # Set X position, leave Y alone
+	   widget.x = 10
+	   # Same here
+	   posi = widget.position
+	   widget.position = (10, posi[1])
+
+	Here they are.
+
+	   - x: Integer: The horizontal part of the position attribute.
+	   - y: Integer: The vertical part of the position attribute.
+	   - width: Integer: The horizontal part of the size attribute.
+	   - height: Integer: The vertical part of the size attribute.
+
+	"""
+
+	ATTRIBUTES = [ Attr('name'), PointAttr('position'),
+		PointAttr('min_size'), PointAttr('size'), PointAttr('max_size'),
+		ColorAttr('base_color'),ColorAttr('background_color'),ColorAttr('foreground_color'),ColorAttr('selection_color'),
+		Attr('style'), Attr('font'),IntAttr('border_size'),Attr('position_technique'),
+		UnicodeAttr('helptext')
+		]
+
+	DEFAULT_NAME = '__unnamed__'
+
+	HIDE_SHOW_ERROR = """\
+		You can only show/hide the top widget of a hierachy.
+		Use 'addChild' or 'removeChild' to add/remove labels for example.
+		"""
+
+	def __init__(self,parent = None, name = DEFAULT_NAME,
+				 size = (-1,-1), min_size=(0,0), max_size=(5000,5000),
+				 helptext=u"",
+				 style = None, **kwargs):
+
+		assert( hasattr(self,'real_widget') )
+		self.event_mapper = events.EventMapper(self)
+		self._visible = False
+
+		# Data distribution & retrieval settings
+		self.accepts_data = False
+		self.accepts_initial_data = False
+
+		self.parent = parent
+
+		# This will also set the _event_id and call real_widget.setActionEventId
+		self.name = name
+
+		self.min_size = min_size
+		self.max_size = max_size
+		self.size = size
+		self.position_technique = "explicit"
+		self.font = 'default'
+
+		# Inherit style
+		if style is None and parent:
+			style = parent.style
+		self.style = style or "default"
+
+		self.helptext = helptext
+		# Not needed as attrib assignment will trigger manager.stylize call
+		#manager.stylize(self,self.style)
+
+	def execute(self,bind):
+		"""
+		Execute a dialog synchronously.
+
+		As argument a dictionary mapping widget names to return values
+		is expected. Events from these widgets will cause this function
+		to return with the associated return value.
+
+		This function will not return until such an event occurs.
+		The widget will be shown before execution and hidden afterwards.
+		You can only execute root widgets.
+
+		Note: This feature is not tested well, and the API will probably
+		change. Otherwise have fun::
+		  # Okay this a very condensed example :-)
+		  return pychan.loadXML("contents/gui/dialog.xml").execute({ 'okButton' : True, 'closeButton' : False })
+
+		"""
+		if not get_manager().can_execute:
+			raise RuntimeError("Synchronous execution is not set up!")
+		if self._parent:
+			raise RuntimeError("You can only 'execute' root widgets, not %s!" % str(self))
+
+		for name,returnValue in bind.items():
+			def _quitThisDialog(returnValue = returnValue ):
+				get_manager().breakFromMainLoop( returnValue )
+				self.hide()
+			self.findChild(name=name).capture( _quitThisDialog )
+		self.show()
+		return get_manager().mainLoop()
+
+	def match(self,**kwargs):
+		"""
+		Matches the widget against a list of key-value pairs.
+		Only if all keys are attributes and their value is the same it returns True.
+		"""
+		for k,v in kwargs.items():
+			if v != getattr(self,k,None):
+				return False
+		return True
+
+	def capture(self, callback, event_name="action", group_name="default"):
+		"""
+		Add a callback to be executed when the widget event occurs on this widget.
+
+		The callback must be either a callable or None.
+		The old event handler (if any) will be overridden by the callback.
+		If None is given, the event will be disabled. You can query L{isCaptured}
+		wether this widgets events are currently captured.
+
+		It might be useful to check out L{tools.callbackWithArguments}.
+
+		@param callback: Event callback - may accept keyword arguments event and widget.
+		@paran event_name: The event to capture - may be one of L{events.EVENTS} and defaults to "action"
+		@paran group_name: Event group.
+
+		Event groups are used to have different B{channels} which don't interfere with each other.
+		For derived widgets that need to capture events it's advised to use the group_name 'widget'.
+		The 'default' group is used by default, and should be reserved for the application programmers.
+		"""
+		self.event_mapper.capture( event_name, callback, group_name )
+
+	def isCaptured(self):
+		"""
+		Check whether this widgets events are captured
+		(a callback is installed) or not.
+		"""
+		return bool(self.event_mapper.getCapturedEvents())
+
+	def show(self):
+		"""
+		Show the widget and all contained widgets.
+		"""
+		if self._parent:
+			raise RuntimeError(Widget.HIDE_SHOW_ERROR)
+		if self._visible: return
+		self.adaptLayout()
+		self.beforeShow()
+		get_manager().show(self)
+		self._visible = True
+
+	def hide(self):
+		"""
+		Hide the widget and all contained widgets.
+		"""
+		if self._parent:
+			raise RuntimeError(Widget.HIDE_SHOW_ERROR)
+		if not self._visible: return
+
+		get_manager().hide(self)
+
+		self.afterHide()
+		self._visible = False
+
+	def isVisible(self):
+		"""
+		Check whether the widget is currently shown,
+		either directly or as part of a container widget.
+		"""
+		widget = self
+		while widget._parent:
+			widget = widget._parent
+		return widget._visible
+
+	def adaptLayout(self,recurse=True):
+		"""
+		Execute the Layout engine. Automatically called by L{show}.
+		In case you want to relayout a visible widget.
+		This function will automatically perform the layout adaption
+		from the top-most layouted widget.
+
+		To make this clear consider this arrangement::
+		      VBox 1
+		      - Container
+			- VBox 2
+			  - HBox
+			    - Label
+
+		If you call adaptLayout on the Label the layout from the VBox 2
+		will get recalculated, while the VBox 1 stays untouched.
+
+		@param recurse Pass False here to force the layout to start from
+		this widget.
+		"""
+		widget = self
+		while widget.parent and recurse:
+			if not isLayouted(widget.parent):
+				break
+			widget = widget.parent
+		widget._recursiveResizeToContent()
+		widget._recursiveExpandContent()
+
+	def beforeShow(self):
+		"""
+		This method is called just before the widget is shown.
+		You can override this in derived widgets to add finalization
+		behaviour.
+		"""
+
+	def afterHide(self):
+		"""
+		This method is called just before the widget is hidden.
+		You can override this in derived widgets to add finalization
+		behaviour.
+		"""
+
+	def findChildren(self,**kwargs):
+		"""
+		Find all contained child widgets by attribute values.
+
+		Usage::
+		  closeButtons = root_widget.findChildren(name='close')
+		  buttons = root_widget.findChildren(__class__=pychan.widgets.Button)
+		"""
+
+		children = []
+		def _childCollector(widget):
+			if widget.match(**kwargs):
+				children.append(widget)
+		self.deepApply(_childCollector)
+		return children
+
+	def findChild(self,**kwargs):
+		""" Find the first contained child widgets by attribute values.
+
+		Usage::
+		  closeButton = root_widget.findChild(name='close')
+		"""
+		children = self.findChildren(**kwargs)
+		if children:
+			return children[0]
+		return None
+
+	def addChild(self,widget):
+		"""
+		This function adds a widget as child widget and is only implemented
+		in container widgets.
+
+		You'll need to call L{adaptLayout} if the container is already shown,
+		to adapt the layout to the new widget. This doesn't happen
+		automatically.
+		"""
+		raise RuntimeError("Trying to add a widget to %s, which doesn't allow this." % repr(self))
+
+	def addChildren(self,*widgets):
+		"""
+		Add multiple widgets as children.
+		Only implemented for container widgets. See also L{addChild}
+
+		Usage::
+			container.addChildren( widget1, widget2, ... )
+			# or you can use this on a list
+			container.addChildren( [widget1,widget2,...] )
+		"""
+		if len(widgets) == 1 and not isinstance(widgets[0],Widget):
+			widgets = widgets[0]
+		for widget in widgets:
+			self.addChild(widget)
+
+	def removeChild(self,widget):
+		"""
+		This function removes a direct child widget and is only implemented
+		in container widgets.
+
+		You'll need to call L{adaptLayout} if the container is already shown,
+		to adapt the layout to the removed widget. This doesn't happen
+		automatically.
+		"""
+		raise RuntimeError("Trying to remove a widget from %s, which is not a container widget." % repr(self))
+
+	def removeChildren(self,*widgets):
+		"""
+		Remove a list of direct child widgets.
+		All widgets have to be direct child widgets.
+		To 'clear' a container take a look at L{removeAllChildren}.
+		See also L{removeChild}.
+
+		Usage::
+			container.removeChildren( widget1, widget2, ... )
+			# or you can use this on a list
+			container.removeChildren( [widget1,widget2,...] )
+		"""
+		if len(widgets) == 1 and not isinstance(widgets[0],Widget):
+			widgets = widgets[0]
+		for widget in widgets:
+			self.removeChild(widget)
+
+	def removeAllChildren(self):
+		"""
+		This function will remove all direct child widgets.
+		This will work even for non-container widgets.
+		"""
+		children = self.findChildren(parent=self)
+		for widget in children:
+			self.removeChild(widget)
+
+	def mapEvents(self,eventMap,ignoreMissing = False):
+		"""
+		Convenience function to map widget events to functions
+		in a batch.
+
+		Subsequent calls of mapEvents will merge events with different
+		widget names and override the previously set callback.
+		You can also pass C{None} instead of a callback, which will
+		disable the event completely.
+
+		@param eventMap: A dictionary with widget/event names as keys and callbacks as values.
+		@param ignoreMissing: Normally this method raises an RuntimeError, when a widget
+		can not be found - this behaviour can be overriden by passing True here.
+
+		The keys in the dictionary are parsed as C{"widgetName/eventName"} with the slash
+		separating the two. If no slash is found the eventName is assumed to be "action".
+
+		Additionally you can supply a group name or channel C{"widgetName/eventName/groupName"}.
+		Event handlers from one group are not overridden by handlers from another group.
+		The default group name is C{"default"}.
+
+		Example::
+			guiElement.mapEvents({
+				"button" : guiElement.hide,
+				"button/mouseEntered" : toggleButtonColorGreen,
+				"button/mouseExited" :  toggleButtonColorBlue,
+			})
+
+		"""
+		for descr,func in eventMap.items():
+			name, event_name, group_name = events.splitEventDescriptor(descr)
+			#print name, event_name, group_name
+			widget = self.findChild(name=name)
+			if widget:
+				widget.capture( func, event_name = event_name, group_name = group_name )
+			elif not ignoreMissing:
+				raise RuntimeError("No widget with the name: %s" % name)
+
+	def setInitialData(self,data):
+		"""
+		Set the initial data on a widget, what this means depends on the Widget.
+		In case the widget does not accept initial data, a L{RuntimeError} is thrown.
+		"""
+		if not self.accepts_initial_data:
+			raise RuntimeError("Trying to set data on a widget that does not accept initial data. Widget: %s Data: %s " % (repr(self),repr(data)))
+		self._realSetInitialData(data)
+
+	def setData(self,data):
+		"""
+		Set the user-mutable data on a widget, what this means depends on the Widget.
+		In case the widget does not accept data, a L{RuntimeError} is thrown.
+		This is inverse to L{getData}.
+		"""
+		if not self.accepts_data:
+			raise RuntimeError("Trying to set data on a widget that does not accept data.")
+		self._realSetData(data)
+
+	def getData(self):
+		"""
+		Get the user-mutable data of a widget, what this means depends on the Widget.
+		In case the widget does not have user mutable data, a L{RuntimeError} is thrown.
+		This is inverse to L{setData}.
+		"""
+		if not self.accepts_data:
+			raise RuntimeError("Trying to retrieve data from a widget that does not accept data.")
+		return self._realGetData()
+
+	def distributeInitialData(self,initialDataMap):
+		"""
+		Distribute B{initial} (not mutable by the user) data from a dictionary over the widgets in the hierachy
+		using the keys as names and the values as the data (which is set via L{setInitialData}).
+		If more than one widget matches - the data is set on ALL matching widgets.
+		By default a missing widget is just ignored.
+
+		Use it like this::
+		  guiElement.distributeInitialData({
+		       'myTextField' : 'Hello World!',
+		       'myListBox' : ["1","2","3"]
+		  })
+
+		"""
+		for name,data in initialDataMap.items():
+			widgetList = self.findChildren(name = name)
+			for widget in widgetList:
+				widget.setInitialData(data)
+
+	def distributeData(self,dataMap):
+		"""
+		Distribute data from a dictionary over the widgets in the hierachy
+		using the keys as names and the values as the data (which is set via L{setData}).
+		This will only accept unique matches.
+
+		Use it like this::
+		  guiElement.distributeData({
+		       'myTextField' : 'Hello World!',
+		       'myListBox' : ["1","2","3"]
+		  })
+
+		"""
+		for name,data in dataMap.items():
+			widgetList = self.findChildren(name = name)
+			if len(widgetList) != 1:
+				if get_manager().debug:
+					self.listNamedWidgets()
+				raise RuntimeError("DistributeData can only handle widgets with unique names.")
+			widgetList[0].setData(data)
+
+	def collectDataAsDict(self,widgetNames):
+		"""
+		Collect data from a widget hierachy by names into a dictionary.
+		This can only handle UNIQUE widget names (in the hierachy)
+		and will raise a RuntimeError if the number of matching widgets
+		is not equal to one.
+
+		Usage::
+		  data = guiElement.collectDataAsDict(['myTextField','myListBox'])
+		  print "You entered:",data['myTextField']," and selected ",data['myListBox']
+
+		"""
+		dataMap = {}
+		for name in widgetNames:
+			widgetList = self.findChildren(name = name)
+			if len(widgetList) != 1:
+				if get_manager().debug:
+					self.listNamedWidgets()
+				raise RuntimeError("CollectData can only handle widgets with unique names.")
+
+			dataMap[name] = widgetList[0].getData()
+		return dataMap
+
+	def collectData(self,*widgetNames):
+		"""
+		Collect data from a widget hierachy by names.
+		This can only handle UNIQUE widget names (in the hierachy)
+		and will raise a RuntimeError if the number of matching widgets
+		is not equal to one.
+
+		This function takes an arbitrary number of widget names and
+		returns a list of the collected data in the same order.
+
+		In case only one argument is given, it will return just the
+		data, with out putting it into a list.
+
+		Usage::
+		  # Multiple element extraction:
+		  text, selected = guiElement.collectData('myTextField','myListBox')
+		  print "You entered:",text," and selected item nr",selected
+		  # Single elements are handled gracefully, too:
+		  test = guiElement.collectData('testElement')
+
+		"""
+		dataList = []
+		for name in widgetNames:
+			widgetList = self.findChildren(name = name)
+			if len(widgetList) != 1:
+				if get_manager().debug:
+					self.listNamedWidgets()
+				raise RuntimeError("CollectData can only handle widgets with unique names.")
+			dataList.append( widgetList[0].getData() )
+		if len(dataList) == 1:
+			return dataList[0]
+		return dataList
+
+	def listNamedWidgets(self):
+		"""
+		This function will print a list of all currently named child-widgets
+		to the standard output. This is useful for debugging purposes.
+		"""
+		def _printNamedWidget(widget):
+			if widget.name != Widget.DEFAULT_NAME:
+				print widget.name.ljust(20),repr(widget).ljust(50),repr(widget._parent)
+		print "Named child widgets of ",repr(self)
+		print "name".ljust(20),"widget".ljust(50),"parent"
+		self.deepApply(_printNamedWidget)
+
+	def stylize(self,style,**kwargs):
+		"""
+		Recursively apply a style to all widgets.
+		"""
+		def _restyle(widget):
+			get_manager().stylize(widget,style,**kwargs)
+		self.deepApply(_restyle)
+
+	def resizeToContent(self,recurse = True):
+		"""
+		Try to shrink the widget, so that it fits closely around its content.
+		Do not call directly.
+		"""
+
+	def expandContent(self,recurse = True):
+		"""
+		Try to expand any spacer in the widget within the current size.
+		Do not call directly.
+		"""
+
+
+	def _recursiveResizeToContent(self):
+		"""
+		Recursively call L{resizeToContent}. Uses L{deepApply}.
+		Do not call directly.
+		"""
+		def _callResizeToContent(widget):
+			#print "RTC:",widget
+			widget.resizeToContent()
+		self.deepApply(_callResizeToContent)
+
+	def _recursiveExpandContent(self):
+		"""
+		Recursively call L{expandContent}. Uses L{deepApply}.
+		Do not call directly.
+		"""
+		def _callExpandContent(widget):
+			#print "ETC:",widget
+			widget.expandContent()
+		self.deepApply(_callExpandContent)
+
+	def deepApply(self,visitorFunc):
+		"""
+		Recursively apply a callable to all contained widgets and then the widget itself.
+		"""
+		visitorFunc(self)
+
+	def sizeChanged(self):
+		pass
+
+	def __str__(self):
+		return "%s(name='%s')" % (self.__class__.__name__,self.name)
+
+	def __repr__(self):
+		return "<%s(name='%s') at %x>" % (self.__class__.__name__,self.name,id(self))
+
+	def _setSize(self,size):
+		if isinstance(size,fife.Point):
+			self.width, self.height = size.x, size.y
+		else:
+			self.width, self.height = size
+
+	def _getSize(self):
+		return self.width, self.height
+
+	def _setPosition(self,size):
+		if isinstance(size,fife.Point):
+			self.x, self.y = size.x, size.y
+		else:
+			self.x, self.y = size
+
+	def _getPosition(self):
+		return self.x, self.y
+
+	def _setX(self,x):self.real_widget.setX(x)
+	def _getX(self): return self.real_widget.getX()
+	def _setY(self,y): self.real_widget.setY(y)
+	def _getY(self): return self.real_widget.getY()
+
+	def _setWidth(self,w):
+		old_width = self.width
+		w = max(self.min_size[0],w)
+		w = min(self.max_size[0],w)
+		self.real_widget.setWidth(w)
+		if w != old_width:
+			self.sizeChanged()
+
+	def _getWidth(self): return self.real_widget.getWidth()
+	def _setHeight(self,h):
+		old_height = self.height
+		h = max(self.min_size[1],h)
+		h = min(self.max_size[1],h)
+		self.real_widget.setHeight(h)
+		if h != old_height:
+			self.sizeChanged()
+
+	def _getHeight(self): return self.real_widget.getHeight()
+
+	def _setFont(self, font):
+		self._font = font
+		self.real_font = get_manager().getFont(font)
+		self.real_widget.setFont(self.real_font)
+	def _getFont(self):
+		return self._font
+
+	def _getBorderSize(self): return self.real_widget.getFrameSize()
+	def _setBorderSize(self,size): self.real_widget.setFrameSize(size)
+
+	base_color = ColorProperty("BaseColor")
+	background_color = ColorProperty("BackgroundColor")
+	foreground_color = ColorProperty("ForegroundColor")
+	selection_color = ColorProperty("SelectionColor")
+
+	def _getStyle(self): return self._style
+	def _setStyle(self,style):
+		self._style = style
+		get_manager().stylize(self,style)
+	style = property(_getStyle,_setStyle)
+
+	def _getParent(self): return self._parent
+	def _setParent(self,parent):
+		self._parent = parent
+	parent = property(_getParent,_setParent)
+
+	def _setName(self,name): self._name = name
+	def _getName(self): return self._name
+	name = property(_getName,_setName)
+
+	x = property(_getX,_setX)
+	y = property(_getY,_setY)
+	width = property(_getWidth,_setWidth)
+	height = property(_getHeight,_setHeight)
+	size = property(_getSize,_setSize)
+	position = property(_getPosition,_setPosition)
+	font = property(_getFont,_setFont)
+	border_size = property(_getBorderSize,_setBorderSize)
+
+	def setEnterCallback(self, cb):
+		"""
+		*DEPRECATED*
+
+		Callback is called when mouse enters the area of Widget
+		callback should have form of function(button)
+		"""
+		if cb is None:
+			self.capture(None, event_name = "mouseEntered" )
+			return
+
+		def callback(widget=None):
+			return cb(widget)
+		print "PyChan: You are using the DEPRECATED functionality: setEnterCallback."
+		self.capture(callback, event_name = "mouseEntered" )
+
+	def setExitCallback(self, cb):
+		"""
+		*DEPRECATED*
+
+		Callback is called when mouse exits the area of Widget
+		callback should have form of function(button)
+		"""
+		if cb is None:
+			self.capture(None, event_name = "mouseExited" )
+			return
+
+		def callback(widget=None):
+			return cb(widget)
+		print "PyChan: You are using the DEPRECATED functionality: setExitCallback."
+		self.capture(callback, event_name = "mouseExited" )
+