# -*- test-case-name: mv3d.test.util.test_guide -*- # Copyright (C) 2010-2012 Mortal Coil Games # See LICENSE for details. """ GUIde - GUI Display Environment GUIde is a framework for quickly designing and iterating on cross-platform, and cross-environment GUIs. For more information, see ticket #420. @author: mike """ import os from xml.etree.ElementTree import parse from twisted.python.log import deferr from twisted.internet.defer import Deferred import mv3d from mv3d.util.math3d import Vector, Quaternion from mv3d.util.classgen import getClass from mv3d.util.enum import Enum from twisted.internet.task import coiterate from twisted.internet import reactor from mv3d.util import getmembers class Direction(Enum): """ Defines a direction for a StackFrame """ horizontal = 0 vertical = 1 class Alignment(Enum): """ Defines an alignment """ center = 0 left = 1 right = 2 top = 3 bottom = 4 class Visibility(Enum): """ Defines the visibility of a widget. """ visible = 0 hidden = 1 collapsed = 2 class DockLocations(Enum): """ Different locations for toolbars """ top = 0 bottom = 1 left = 2 right = 3 center = 4 float = 5 #@ReservedAssignment centerPane = 6 class ToolbarItemType(Enum): """ Defines different types of toolbar items """ button = 0 separator = 1 label = 2 spacer = 3 radio = 4 check = 5 class ToolbarTextLocations(Enum): """ Defines locations for text on the toolbar items. """ left = 0 right = 1 top = 2 bottom = 3 class MenuItemType(Enum): """ Types of menu items. """ normal = 0 check = 1 radio = 2 separator = 3 class BorderType(Enum): """ Different types of borders that can be used. """ noBorder = 0 default = 1 normal = 2 simple = 3 double = 4 sunken = 5 raised = 6 class PropertyDataTypes(Enum): """ Defines the available property datatypes. """ string = 0 integer = 1 float = 2 #@ReservedAssignment boolean = 3 multiLineString = 4 directory = 5 file = 6 #@ReservedAssignment stringArray = 7 enum = 8 date = 9 color = 10 imageFile = 11 choide = 12 multipleChoice = 13 custom = 14 hidden = 15 class FloatConverter(object): """ Converts strings to floats """ def __init__(self, default=0.0): self.default = default def convertTo(self, value): """ stringify """ return str(value) def convertFrom(self, value): """ floatify """ try: return float(value) except: return self.default class IntConverter(object): """ Converts strings to ints """ def __init__(self, default=0): self.default = default def convertTo(self, value): """ stringify """ return str(value) def convertFrom(self, value): """ intify """ try: return int(value) except TypeError: return self.default class VectorConverter(object): """ Converts strings to vectors """ def __init__(self, default=0.0): self.default = default def convertTo(self, value): """ stringify """ if isinstance(value, (str, unicode)): return value return ", ".join([str(val) for val in value]) def convertFrom(self, value): """ vectorify """ if isinstance(value, (str, unicode)): try: return Vector([float(val.strip()) for val in value.split(",")]) except ValueError: return self.default elif isinstance(value, (Vector, tuple)): return value return self.default class DataAccessor(object): _lastBinding = None def __init__(self, baseAttr, coerce=None, default=None): self.baseAttr = baseAttr self.coerce = coerce self.default = default def __get__(self, obj, type=None): if obj is None: return self elif getattr(obj, self.baseAttr, None) is None: return self.default if self.coerce is None: return getattr(obj, self.baseAttr).getData() try: return self.coerce(getattr(obj, self.baseAttr).getData(), obj, self.baseAttr) except TypeError: # mdh: this isn't great. return self.coerce(getattr(obj, self.baseAttr).getData()) def __set__(self, obj, value): if isinstance(value, Data): if self.baseAttr != "_dataContext": value.widget = obj setattr(obj, self.baseAttr, value) elif getattr(obj, self.baseAttr, None) is None: setattr(obj, self.baseAttr, Data(value)) else: getattr(obj, self.baseAttr).setData(value) if hasattr(obj, "propertyChanged"): if self._lastBinding is not None: self._lastBinding[0].stopListening(self._dataChanged) self._lastBinding = getattr(obj, self.baseAttr), obj getattr(obj, self.baseAttr).listen(self._dataChanged) obj.propertyChanged(self.baseAttr[1:]) def _dataChanged(self, _obj, _prop): self._lastBinding[1].propertyChanged(self.baseAttr[1:]) class Coordinate(object): """ Defines a coordinate which can be relative or absolute """ relative = 0 absolute = 0 value = None def __init__(self, value=None, relative=0, absolute=0): self.relative = relative self.absolute = absolute if value is not None: if isinstance(value, (str, unicode)): value = value.strip() if value.endswith("%"): self.value = int(value[:-1]) self.relative = self.value else: self.value = int(value) self.absolute = int(value) elif isinstance(value, Coordinate): self.relative = value.relative self.absolute = value.absolute self.value = value.value else: self.value = int(value) self.absolute = int(value) def __str__(self): """ Stringify the coordinate """ return "Coordinate(relative=%.2f, absolute=%.2f)" % (self.relative, self.absolute) def __add__(self, other): """ Add two coordinates together """ try: return Coordinate(None, self.relative + other.relative, self.absolute + other.absolute) except AttributeError: # assume it's a number return Coordinate(None, self.relative, self.absolute + other) def __sub__(self, other): """ Add two coordinates together """ try: return Coordinate(None, self.relative - other.relative, self.absolute - other.absolute) except AttributeError: # assume it's a number return Coordinate(None, self.relative, self.absolute - other) def __div__(self, other): """ Divide by a number """ return Coordinate(relative=self.relative / other, absolute=self.absolute / other) def __mul__(self, other): """ Multiply by a number """ return Coordinate(relative=self.relative * other, absolute=self.absolute * other) def __rmul__(self, other): """ Multiply by a number """ return Coordinate(relative=self.relative * other, absolute=self.absolute * other) def __radd__(self, other): """ Add two coordinates together """ try: return Coordinate(None, self.relative + other.relative, self.absolute + other.absolute) except AttributeError: # assume it's a number return Coordinate(None, self.relative, self.absolute + other) def __rsub__(self, other): """ Add two coordinates together """ try: return Coordinate(None, other.relative - self.relative, other.absolute - self.absolute) except AttributeError: # assume it's a number return Coordinate(None, self.relative, other - self.absolute) def __eq__(self, other): """ Compare two coordinates """ try: if self.absolute != other.absolute: return False return self.relative == other.relative except AttributeError: if self.relative: return False return self.absolute == other def __ne__(self, other): """ Compare two coordinates """ try: if self.absolute != other.absolute: return True return self.relative != other.relative except AttributeError: if self.relative: return True return self.absolute != other def __gt__(self, other): """ Compare two coordinates """ return self.asAbsolute(100) > other.asAbsolute(100) def __lt__(self, other): """ Compare two coordinates """ return self.asAbsolute(100) < other.asAbsolute(100) def __ge__(self, other): """ Compare two coordinates """ return self.asAbsolute(100) >= other.asAbsolute(100) def __le__(self, other): """ Compare two coordinates """ return self.asAbsolute(100) <= other.asAbsolute(100) def __neg__(self): """ Negate the coordinate """ return Coordinate(relative= -self.relative, absolute= -self.absolute) def asAbsolute(self, absoluteParentSize): """ Convert to an absolute value """ return (self.relative / 100.0) * absoluteParentSize + self.absolute def asRelative(self, absoluteParentSize): """ Convert to a relative value """ return (self.absolute / float(absoluteParentSize)) * 100 + self.relative def coordinate(incoming, _obj, _attr): if incoming is None: return None if isinstance(incoming, Coordinate): return incoming return Coordinate(incoming) def strstrip(incoming, _obj, _attr): """ Stringify and strip. """ if incoming is None: return None return str(incoming).strip() def floatOrNone(incoming, _obj, _attr): """ Floatify incoming """ if incoming is None: return None return float(incoming) def intOrNone(incoming, _obj, _attr): """ Intify incoming """ if incoming is None: return None return int(incoming) def listOrNone(incoming, _obj, _attr): """ Listify incoming """ if incoming is None: return None return list(incoming) def boolify(incoming, _obj, _attr): """ boolify incoming """ if incoming is None: return None if isinstance(incoming, str): incoming = incoming.strip().lower() if incoming in [1, True, "true", "yes"]: return True if incoming in [0, False, "false", "no"]: return False return bool(incoming) def makeVector(incoming, _obj, _attr): """ Turn incoming into a vector """ if incoming is None: return None elif len(incoming) == 3: return incoming return Vector(tuple([float(axis.strip()) for axis in incoming.split(",")])) def makeQuaternion(incoming, _obj, _attr): """ Turn incoming into a quaternion """ if incoming is None: return None elif len(incoming) == 4: return incoming return Quaternion(tuple([float(axis.strip()) for axis in incoming.split(",")])) def makeColor(incoming, _obj, _attr): """ Turn incoming into a color """ if incoming is None: return None elif len(incoming) == 4: return incoming elif len(incoming) == 3: return tuple(list(incoming) + [1.0]) incoming = tuple([float(axis.strip()) for axis in incoming.split(",")]) if len(incoming) == 3: return tuple(list(incoming) + [1.0]) return incoming def enumeration(enumClass, default=None): """ Converts an enum item (string) to its value. """ def convertEnum(incoming, _obj, _attr): if incoming is None: return default if isinstance(incoming, (str, unicode)): return getattr(enumClass, incoming) return incoming return convertEnum class ChangeNotifier(object): """ Interface for notification when a property of the object is changed. """ _propertyChangedCallbacks = None errorHandler = None def addPropertyListener(self, property, callback): """ Register a property listener-- a function that takes two args. The first is the object being changed (self), and the second is the property name that has changed. """ if hasattr(self, "_" + property): prop = getattr(self, "_" + property) if isinstance(prop, EventHandler): prop.listen(callback) return if self._propertyChangedCallbacks is None: self._propertyChangedCallbacks = {} if not self._propertyChangedCallbacks.has_key(property): self._propertyChangedCallbacks[property] = [] self._propertyChangedCallbacks[property].append(callback) return PropertyListener(self, property, callback) listen = addPropertyListener def removePropertyListener(self, property, callback): """ Removes a previously added property listener """ if hasattr(self, "_" + property): prop = getattr(self, "_" + property) if isinstance(prop, EventHandler): prop.stopListening(callback) return try: self._propertyChangedCallbacks[property].remove(callback) except ValueError: pass def propertyChanged(self, property): """ Call this when a property has changed that you want to notify about """ if self._propertyChangedCallbacks is None: return cbs = self._propertyChangedCallbacks.get(property, [])[:] cbs.extend(self._propertyChangedCallbacks.get(None, [])) for callback in cbs[:]: try: res = callback(self, property) if isinstance(res, Deferred): if self.errorHandler is not None: res.addErrback(self.errorHandler) elif hasattr(self, "parser"): res.addErrback(self.parser.errorHandler) else: res.addErrback(deferr) except: if self.errorHandler is not None: self.errorHandler() elif hasattr(self, "parser") and self.parser is not None: self.parser.errorHandler() else: deferr() class NotifierProperty(object): """ Auto-notifies when the property is set. This can be used within a ChangeNotifier object in order to simplify notifications for property changes. """ def __init__(self, attrib, default=None, converter=None, dataType=None, section="Properties", dataTypeParam=None): self.converter = converter self.attrib = attrib self.valueAttrib = "_" + attrib if converter is not None: default = converter.convertFrom(default) self.default = default self.dataType = dataType self.section = section self.dataTypeParam = dataTypeParam def __get__(self, obj, type=None): """ Getter which should return either the default or the current value. """ if obj is None: return self if not hasattr(obj, self.valueAttrib): return self.default if self.converter is not None: return self.converter.convertFrom(getattr(obj, self.valueAttrib)) return getattr(obj, self.valueAttrib) def __set__(self, obj, value): """ Setter which will cause the property changed event to be fired """ if self.converter is not None: value = self.converter.convertTo(value) setattr(obj, self.valueAttrib, value) obj.propertyChanged(self.attrib) class Widget(object): """ The base of all widgets. """ dataContext = DataAccessor("_dataContext") name = DataAccessor("_name", strstrip) text = DataAccessor("_text", str) top = DataAccessor("_top", coordinate)#("height")) left = DataAccessor("_left", coordinate)#("width")) width = DataAccessor("_width", coordinate)#("width")) height = DataAccessor("_height", coordinate)#("height")) topIsRelative = DataAccessor("_topIsRelative", boolify, default=False) leftIsRelative = DataAccessor("_leftIsRelative", boolify, default=False) widthIsRelative = DataAccessor("_widthIsRelative", boolify, default=False) heightIsRelative = DataAccessor("_heightIsRelative", boolify, default=False) halign = DataAccessor("_halign", enumeration(Alignment, Alignment.left)) valign = DataAccessor("_valign", enumeration(Alignment, Alignment.top)) visibility = DataAccessor("_visibility", enumeration(Visibility, Visibility.visible)) enabled = DataAccessor("_enabled", boolify) contextMenu = DataAccessor("_contextMenu", default=None) backgroundColor = DataAccessor("_backgroundColor", makeColor, default=None) textColor = DataAccessor("_textColor", makeColor, default=None) tooltip = DataAccessor("_tooltip", default=None) borderType = DataAccessor("_borderType", enumeration(BorderType, None)) border = DataAccessor("_border", intOrNone) parent = None destroyed = False _nextTickUpdateCall = None _nextTickCalls = None def __init__(self, name=None, text="", width=None, height=None, top=0, left=0, children=None, halign=Alignment.left, valign=Alignment.top, dataContext=None, parent=None, onLoad=None, visibility="visible", enabled=True, contextMenu=None, tooltip=None, onShow=None, onHide=None, backgroundColor=None, textColor=None, onKeyUp=None, onLeftUp=None, onLeftDown=None, onRightUp=None, onRightDown=None, onMouseMoved=None, onKeyDown=None, border=None, borderType=None): self._waitingChildren = [] self.listeners = {} self.text = text self.dataContext = dataContext self.children = children or [] self.width = width self.height = height self.top = top self.left = left self.halign = halign self.valign = valign self.name = name self.visibility = visibility self.enabled = enabled self.contextMenu = contextMenu self.backgroundColor = backgroundColor self.textColor = textColor self.tooltip = tooltip self.border = border self.borderType = borderType self.parent = parent self.listen("dataContext", self.dataContextChanged) self._onLoad = EventHandler() if onLoad is not None: self.listen("onLoad", onLoad) self._onShow = EventHandler() if onShow is not None: self.listen("onShow", onShow) self._onHide = EventHandler() if onHide is not None: self.listen("onHide", onHide) self._onKeyUp = EventHandler() if onKeyUp is not None: self.listen("onKeyUp", onKeyUp) self._onKeyDown = EventHandler() if onKeyDown is not None: self.listen("onKeyDown", onKeyDown) self._onLeftUp = EventHandler() if onLeftUp is not None: self.listen("onLeftUp", onLeftUp) self._onLeftDown = EventHandler() if onLeftDown is not None: self.listen("onLeftDown", onLeftDown) self._onRightUp = EventHandler() if onRightUp is not None: self.listen("onRightUp", onRightUp) self._onRightDown = EventHandler() if onRightDown is not None: self.listen("onRightDown", onRightDown) self._onMouseMoved = EventHandler() if onMouseMoved is not None: self.listen("onMouseMoved", onMouseMoved) self.listen("enabled", self._enabledChanged) self.listen("width", self._sizeChanged) self.listen("height", self._sizeChanged) if parent is not None and hasattr(parent, "_waitingChildren"): self.parent._waitingChildren.append(self) def _enabledChanged(self, _obj, _prop): """ The enabled state of this widget changed """ if self.enabled: self.enable() else: self.disable() def _sizeChanged(self, _obj, _prop): """ Called when the size of the widget changes. """ self.callNextTick(self._notifyChildrenOfResize) def _notifyChildrenOfResize(self): """ Iterate through children and let them know we've been resized """ for child in self.children: child.onParentResized() def _callNextTickCalls(self): """ Call all the calls that were delayed a tick """ self._nextTickUpdateCall = None for func, args, kwargs in self._nextTickCalls: func(*args, **dict(kwargs)) def callNextTick(self, func, *args, **kwargs): """ Schedule a call to be made at the next opportunity. Duplicate calls will be combined. """ key = (func, args, kwargs.items()) if self._nextTickCalls is None: self._nextTickCalls = [] elif key in self._nextTickCalls: return False if self._nextTickUpdateCall is None: self._nextTickUpdateCall = reactor.callLater(0.1, #@UndefinedVariable self._callNextTickCalls) self._nextTickCalls.append(key) return True def onKeyUp(self, key): """ Send the event """ self._onKeyUp(self, key) def onKeyDown(self, key): """ Send the event """ self._onKeyDown(self, key) def onLeftUp(self, pos): """ Send the event """ self._onLeftUp(self, pos) def onLeftDown(self, pos): """ Send the event """ self._onLeftDown(self, pos) def onRightDown(self, pos): """ Send the event """ self._onRightDown(self, pos) def onRightUp(self, pos): """ Send the event """ self._onRightUp(self, pos) def onMouseMoved(self, pos): """ Send the event """ self._onMouseMoved(self, pos) def onLoad(self): """ Called when loaded """ self._onLoad(self) def listen(self, event, callback): """ Listen for a change event and callback when it happens """ attr = getattr(self, "_" + event) attr.listen(callback) if not self.listeners.has_key("_" + event): self.listeners["_" + event] = [] if callback not in self.listeners["_" + event]: self.listeners["_" + event].append(callback) def stopListening(self, event, callback): """ Stop listening to a previously requested event """ attr = getattr(self, "_" + event) attr.stopListening(callback) self.listeners["_" + event].remove(callback) def onGetChildren(self, parser): """ Called once our children have been parsed """ if isinstance(self.children, TemplateFactory): self.children = self.children.produce(self.dataContext, self) self._waitingChildren = [] def initialize(self, parser=None): """ Call on the base widget when creating widgets from code """ if parser is None: parser = self.getParser() for child in self._waitingChildren: child.initialize(parser) self.children.extend(self._waitingChildren) self.onGetChildren(parser) def dataContextChanged(self, caller=None, data=None): """ Called when the datacontext is modified. Basically, we need to fire all events related to that datacontext. """ for listeners in self.listeners.values(): for listener in listeners: if listener != self.dataContextChanged: listener(caller, data) for child in self.children: child.dataContextChanged(caller, data) def destroy(self): """ Called to destroy the widget. You may want to override in a subclass. """ for key, value in getmembers(self, lambda x: isinstance(x, Data)): if hasattr(value, "listeners") and value.widget == self: for listener in value.listeners: listener.remove() for key, value in self.listeners.items(): try: getattr(self, key).stopListening(value) except ValueError: continue self.listeners = {} for child in self.children: child.destroy() if self._nextTickUpdateCall is not None: self._nextTickUpdateCall.cancel() self._nextTickUpdateCall = None self.destroyed = True def getChild(self, name): """ Returns the child with the specified name """ for child in self.children: if child.name == name: return child raise KeyError(name) def getSibling(self, name): """ Returns a sibling of this widget """ return self.parent.getChild(name) def findChild(self, value, attr="name"): """ Recursively search until we find the child or return None if not found. If attr is specified, the child will be selected by that attribute matching the specified value. """ for child in self.children: if hasattr(child, attr) and getattr(child, attr) == value: return child result = child.findChild(value, attr) if result is not None: return result return None def findParent(self, cls): """ Search up the tree until we hit a widget of the specified class. """ widget = self.parent while widget is not None and not isinstance(widget, cls): widget = widget.parent return widget def getChildrenRecursive(self): """ Returns a generator that iterates through all children recursively. """ for child in self.children: yield child for grandChild in child.getChildrenRecursive(): yield grandChild def findChildrenWithAttribute(self, attr): """ Returns a generator that iterates through all children with the specified attribute. """ for child in self.getChildrenRecursive(): if hasattr(child, attr): yield child def findChildrenOfClass(self, cls): """ Returns a generator that iterates through all children that match a class (or subclass) """ for child in self.getChildrenRecursive(): if isinstance(child, cls): yield child def findChildren(self, value, attr="name"): """ Returns a generator that iterates through all children where the attr of the child matches the value. Defaults to searching by name. """ for child in self.getChildrenRecursive(): if hasattr(child, attr) and getattr(child, attr) == value: yield child def getParser(self): """ Search up the tree until we hit the parser. """ return self.findParent(Parser) def disable(self): """ Disable the widget """ def enable(self): """ Enable the widget """ def show(self): """ Show the widget """ self.visibility = Visibility.visible self._onShow(self) def hide(self): """ Hide the widget """ self.visibility = Visibility.hidden self._onHide(self) def onParentResized(self): """ Called when the parent was resized """ class CustomControlFrame(Widget): """ Holder for a custom control. Only inherit from this in guide implementations, not to define a custom control. """ def __init__(self, controlClass, **attribs): self.customControl = controlClass(self, **attribs) def postInit(self, **attribs): """ Called from the __init__ of the custom control. This is required. """ Widget.__init__(self, **attribs) def onGetChildren(self, parser): """ Called when children have been successfully parsed or have changed. """ Widget.onGetChildren(self, parser) self.customControl.onGetChildren(parser) def destroy(self): """ Destroy this widget """ Widget.destroy(self) self.customControl.destroy() self.customControl = None class CustomControl(ChangeNotifier): """ Defines a custom control. Inherit from this to implement a new custom control. """ dataContext = DataAccessor("_dataContext") def __init__(self, frame, **attribs): self.frame = frame if hasattr(self.frame, "postInit"): self.frame.postInit(**attribs) self._dataContext = self.frame._dataContext def onGetChildren(self, parser): """ Called when children have been successfully parsed or have changed. """ def destroy(self): """ Destroy this control. """ class Frame(Widget): """ A frame widget """ class Button(Widget): """ A button widget """ isToggle = DataAccessor("_isToggle", boolify) isDown = DataAccessor("_isDown", boolify) image = DataAccessor("_image", default=None) hoverImage = DataAccessor("_hoverImage", default=None) pressedImage = DataAccessor("_pressedImage", default=None) border = DataAccessor("_border", boolify) def __init__(self, onClick=None, isToggle=False, isDown=False, image=None, border=True, hoverImage=None, pressedImage=None, **kwargs): Widget.__init__(self, **kwargs) self.isToggle = isToggle self.isDown = isDown self.image = image self.hoverImage = hoverImage self.pressedImage = pressedImage self.border = border self._click = EventHandler() if onClick is not None: self.listen("click", onClick) def click(self): """ Send a click event. """ self._click(self) class TextBox(Widget): """ A textbox widget """ multiline = DataAccessor("_multiline", boolify) editable = DataAccessor("_editable", boolify) mask = DataAccessor("_mask", boolify) isLog = DataAccessor("_isLog", boolify) cursorPosition = DataAccessor("_cursorPosition", intOrNone) def __init__(self, onEnter=None, multiline=None, editable=True, mask=False, isLog=False, cursorPosition=0, **attribs): Widget.__init__(self, **attribs) self.multiline = multiline self.editable = editable self.mask = mask self.isLog = isLog self.cursorPosition = cursorPosition self._onEnter = EventHandler() if onEnter is not None: self.listen("onEnter", onEnter) def onEnter(self): """ Send the event """ self._onEnter(self, self.text) def insert(self, text, position=None): """ Insert text at the specified position. """ if position is None: position = self.cursorPosition self.text = self.text[:position] + text + self.text[position:] class Spinner(Widget): """ A spinner widget """ editable = DataAccessor("_editable", boolify) minimum = DataAccessor("_minimum", floatOrNone) maximum = DataAccessor("_maximum", floatOrNone) increment = DataAccessor("_increment", floatOrNone) decimalPlaces = DataAccessor("_decimalPlaces", intOrNone) value = DataAccessor("_value", floatOrNone) def __init__(self, onEnter=None, minimum=None, maximum=None, editable=True, increment=1.0, decimalPlaces=0, value=0, **attribs): Widget.__init__(self, **attribs) self.minimum = minimum self.maximum = maximum self.editable = editable self.increment = increment self.decimalPlaces = decimalPlaces self.value = value self._onEnter = EventHandler() if onEnter is not None: self.listen("onEnter", onEnter) def onEnter(self): """ Send the event """ self._onEnter(self, self.text) class Label(Widget): """ A label widget """ class ProgressBar(Widget): """ A progress bar """ progress = DataAccessor("_progress", floatOrNone) def __init__(self, progress=0.0, **attribs): Widget.__init__(self, **attribs) self.progress = progress class CheckBox(Widget): """ A checkbox widget """ checked = DataAccessor("_checked", bool) def __init__(self, checked=False, **attribs): Widget.__init__(self, **attribs) self.checked = checked class Slider(Widget): """ A slider widget """ start = DataAccessor("_start", floatOrNone) end = DataAccessor("_end", floatOrNone) value = DataAccessor("_value", floatOrNone) def __init__(self, start=0.0, end=1.0, value=0.0, **attribs): Widget.__init__(self, **attribs) self.start = start self.end = end self.value = value class ComboBox(Widget): """ A combobox widget """ items = DataAccessor("_items", listOrNone) itemTemplate = DataAccessor("_itemTemplate") editable = DataAccessor("_editable", boolify) selection = DataAccessor("_selection") def __init__(self, items=None, itemTemplate=None, onSelect=None, editable=True, selection=None, **attribs): Widget.__init__(self, **attribs) self.items = items or [] self.itemTemplate = itemTemplate self.editable = editable self.selection = selection self._onSelect = EventHandler() if onSelect is not None: self.listen("onSelect", onSelect) def onSelect(self, selection): """ The selection was changed """ self._onSelect(self, selection) def onGetChildren(self, parser): """ Called once our children have been parsed, make a list of items. """ Widget.onGetChildren(self, parser) for child in self.children[:]: if isinstance(child, ComboBoxItem): self.items += [child.text] class ComboBoxItem(Widget): """ An item in a combobox """ class ListBox(Widget): """ A combobox widget """ items = DataAccessor("_items", listOrNone) itemTemplate = DataAccessor("_itemTemplate") selection = DataAccessor("_selection") selectMultiple = DataAccessor("_selectMultiple") columns = DataAccessor("_columns") firstSelection = DataAccessor("_firstSelection") def __init__(self, items=None, itemTemplate=None, selection=None, columns=None, onSelect=None, onDoubleClick=None, selectMultiple=False, firstSelection=None, **attribs): Widget.__init__(self, **attribs) self.items = items or [] self.itemTemplate = itemTemplate self.selection = selection self.selectMultiple = selectMultiple self.columns = columns self.firstSelection = firstSelection self._onSelect = EventHandler() if onSelect is not None: self.listen("onSelect", onSelect) self._onDoubleClick = EventHandler() if onDoubleClick is not None: self.listen("onDoubleClick", onDoubleClick) def onSelect(self, selection): """ The selection was changed """ if self.selectMultiple: try: self.firstSelection = selection[0] except IndexError: self.firstSelection = None else: self.firstSelection = selection self._onSelect(self, selection) def onDoubleClick(self, selection): """ The selection was changed """ self._onDoubleClick(self, selection) def onGetChildren(self, parser): """ Called once our children have been parsed, make a list of items """ Widget.onGetChildren(self, parser) for child in self.children[:]: if isinstance(child, ListBoxItem): if self.columns is not None: if len(child.children): row = [] for grandChild in child.children: row.append(grandChild.text) self.items += [row] else: self.items += [child.text] class ListBoxItem(Widget): """ An item in a combobox """ class ListBoxColumn(Widget): """ A column on a listbox """ def __init__(self, field=None, **attribs): Widget.__init__(self, **attribs) self.field = field class _ListFrameChildBuilder(object): """ This is used with a cooperator in order to distribute creating list frame items over time. """ def __init__(self, listFrame, useCache): self.listFrame = listFrame self.useCache = useCache self.childDict = dict(zip( [child.dataContext for child in listFrame.children], listFrame.children)) self.listFrame.children = [] self.itemIter = iter(listFrame.items or []) self.listFrame.updateProgress = 0.0 if listFrame.items: self.progressIncr = 1.0 / len(listFrame.items) else: self.progressIncr = 1.0 def next(self): #@ReservedAssignment """ Process the next item in the list. Raises StopIteration when done. """ try: item = self.itemIter.next() if self.useCache: try: child = self.childDict[item] del self.childDict[item] self.listFrame.children.append(child) except KeyError: self.listFrame.children.extend( self.listFrame.itemTemplate.produce(item, self.listFrame)) else: self.listFrame.children.extend( self.listFrame.itemTemplate.produce(item, self.listFrame)) self.listFrame.updateProgress += self.progressIncr except StopIteration: for oldChild in self.childDict.values(): oldChild.destroy() raise class ListFrame(Widget): """ A list frame widget. Basically a stack frame where the items are templated and pulled from a list. """ items = DataAccessor("_items", listOrNone) itemTemplate = DataAccessor("_itemTemplate") direction = DataAccessor("_direction", enumeration(Direction, default=Direction.vertical)) selection = DataAccessor("_selection") isSelectable = DataAccessor("_isSelectable", boolify, default=False) selectionBackgroundColor = DataAccessor("_selectionBackgroundColor") selectMultiple = DataAccessor("_selectMultiple", boolify, default=False) alternatingBackgroundColor = DataAccessor("_alternatingBackgroundColor") updateProgress = DataAccessor("_updateProgress") populateDeferred = None updating = False def __init__(self, items=None, itemTemplate=None, direction=None, selection=None, isSelectable=False, selectionBackgroundColor=(0, 0, 255), selectMultiple=False, alternatingBackgroundColor=None, **attribs): Widget.__init__(self, **attribs) self.direction = direction self.items = items or [] self.itemTemplate = itemTemplate self.selection = selection self.isSelectable = isSelectable self.selectionBackgroundColor = selectionBackgroundColor self.selectMultiple = selectMultiple self.alternatingBackgroundColor = alternatingBackgroundColor self.updateProgress = 0.0 self.listen("items", self._itemsChanged) self.listen("itemTemplate", self._templateChanged) def onGetChildren(self, parser, useCache=True): """ Rebuild the list. NOTE: this returns a deferred! """ Widget.onGetChildren(self, parser) if self.populateDeferred is not None: # don't double stack these def goAgain(res): self.onGetChildren(parser, useCache) self.populateDeferred.addCallback(goAgain) return def populateDone(res): self.populateDeferred = None return res def populateError(err): if hasattr(self.dataContext, "errorHandler") and callable(self.dataContext.errorHandler): self.dataContext.errorHandler(err) else: self.getParser().errorHandler(err) return err if self.items: self.populateDeferred = coiterate(_ListFrameChildBuilder(self, useCache)) self.populateDeferred.addCallback(populateDone) self.populateDeferred.addErrback(populateError) return self.populateDeferred else: for child in self.children: child.destroy() self.children = [] def _itemsChanged(self, _obj, _property): """ Called when the list of items changes """ if self.updating: return if hasattr(self, "window") and self.window is None: # TODO: figure out why this callback is never removed! #self.stopListening("items", self.itemsChanged) # KeyError return return self.onGetChildren(self.getParser()) def _templateChanged(self, _obj, _prop): """ Called when the template changes """ if self.window is None: # TODO: figure out why this callback is never removed! #self.stopListening("items", self.itemsChanged) # KeyError return return self.onGetChildren(self.itemTemplate.parser, useCache=False) class ListFrameItem(Widget): """ An item in a list frame """ class PythonConsole(Widget): """ An interactive python console widget """ locals = DataAccessor("_locals") def __init__(self, locals=None, **attribs): Widget.__init__(self, **attribs) self.locals = locals class StackFrame(Widget): """ A stack frame """ direction = DataAccessor("_direction", enumeration(Direction, default=Direction.vertical)) def __init__(self, direction=None, **attribs): Widget.__init__(self, **attribs) self.direction = direction class WrapFrame(Widget): """ A wrap frame """ class Position(Widget): """ A widget that holds a position """ vector = DataAccessor("_vector", makeVector) renderFrame = DataAccessor("_renderFrame") showGizmo = DataAccessor("_showGizmo", boolify) def __init__(self, vector=(0, 0, 0), renderFrame=None, showGizmo=False, **attribs): Widget.__init__(self, **attribs) self.vector = vector self.renderFrame = renderFrame self.showGizmo = showGizmo class Orientation(Widget): """ A widget that holds an orientation """ euler = DataAccessor("_euler", makeVector) quaternion = DataAccessor("_quaternion", makeQuaternion) renderFrame = DataAccessor("_renderFrame") showGizmo = DataAccessor("_showGizmo", boolify) units = DataAccessor("_units") def __init__(self, euler=None, renderFrame=None, showGizmo=False, quaternion=None, units="radians", **attribs): Widget.__init__(self, **attribs) self.euler = euler self.quaternion = quaternion self.renderFrame = renderFrame self.showGizmo = showGizmo self.units = units class SplitFrame(Widget): """ A split frame """ direction = DataAccessor("_direction", enumeration(Direction, default=Direction.vertical)) sashOffset = DataAccessor("_sashOffset", floatOrNone) def __init__(self, direction=None, sashOffset=0, **attribs): Widget.__init__(self, **attribs) self.direction = direction self.sashOffset = sashOffset class ScrollFrame(Widget): """ A scrolled frame """ class GroupFrame(Widget): """ A group frame """ direction = DataAccessor("_direction", enumeration(Direction, default=Direction.vertical)) def __init__(self, direction=None, **attribs): Widget.__init__(self, **attribs) self.direction = direction class GridRow(Widget): """ Holds a grid row """ rowspan = DataAccessor("_rowspan", int) def __init__(self, rowspan=1, **kwargs): Widget.__init__(self, **kwargs) self.rowspan = rowspan class GridCell(Widget): """ Holds a cell of a grid """ colspan = DataAccessor("_colspan", int) def __init__(self, colspan=1, **kwargs): Widget.__init__(self, **kwargs) self.colspan = colspan class GridFrame(Widget): """ A grid frame """ def _columnCount(self): """ Returns the maximum number of columns across all rows """ return max([len(child.children) for child in self.children]) columnCount = property(_columnCount) def _rowCount(self): """ Returns the number of rows """ return len(self.children) rowCount = property(_rowCount) class PropertyGridField(Widget): """ Defines a field for a property grid. """ dataType = DataAccessor("_dataType", enumeration(PropertyDataTypes, PropertyDataTypes.string)) section = DataAccessor("_section") field = DataAccessor("_field") typeParameters = DataAccessor("_typeParameters") priority = DataAccessor("_priority") valueAsString = DataAccessor("_valueAsString") def __init__(self, dataType=None, section="Properties", field=None, typeParameters=None, priority=0, valueAsString=False, **args): Widget.__init__(self, **args) self.dataType = dataType self.section = section self.field = field self.typeParameters = typeParameters self.priority = priority self.valueAsString = valueAsString def _getIsHidden(self): """ Returns true if this field should be hidden. """ return self.dataType == PropertyDataTypes.hidden isHidden = property(_getIsHidden) def getEnumNamesAndValues(self): """ Interprets the typeParameters for an enum field and returns names and values. """ if isinstance(self.typeParameters, Enum): names = self.typeParameters.keys() values = self.typeParameters.values() elif isinstance(self.typeParameters, (tuple, list)): names = type.fieldParameters values = range(len(type.fieldParameters)) self.valueAsString = True elif isinstance(self.typeParameters, (str, unicode)): # either an enum class ref or a list of strings if "," in self.typeParameters: names = self.typeParameters.split(",") values = range(len(names)) self.valueAsString = True else: cls = getClass(self.typeParameters) names = cls.keys() values = cls.values() return names, values class PropertyGrid(Widget): """ Displays a property grid that can be used to edit the properties of a Python object. Fields can be described manually within the xml or they can be automatically determined if the object is Persistable. """ value = DataAccessor("_value") fields = DataAccessor("_fields") _currentFields = None _scrollFrame = None # This needs to be overridden in a subclass. dataTypeMap = { PropertyDataTypes.string:TextBox, PropertyDataTypes.integer:Spinner, PropertyDataTypes.float:Spinner, PropertyDataTypes.boolean:CheckBox, PropertyDataTypes.multiLineString:TextBox, PropertyDataTypes.directory:TextBox, PropertyDataTypes.file:TextBox, PropertyDataTypes.stringArray:TextBox, PropertyDataTypes.enum:ComboBox, PropertyDataTypes.date:TextBox, PropertyDataTypes.color:TextBox, PropertyDataTypes.imageFile:TextBox, PropertyDataTypes.multipleChoice:TextBox, } def __init__(self, value=None, fields=None, onPropertyEdited=None, **attribs): Widget.__init__(self, **attribs) self.value = value self.fields = fields self._onPropertyEdited = EventHandler() if onPropertyEdited is not None: self.listen("onPropertyEdited", onPropertyEdited) def _getFields(self): """ Returns the fields of the value object. """ if isinstance(self.fields, TemplateFactory): self.fields = self.fields.produce(self.dataContext, self) fields = {} if hasattr(self.value, "_attributes"): # persistable for key, value in self.value._attributes.items(): if getattr(value, "editDataType", None): field = PropertyGridField(value.editDataType, "Properties", key) else: # determine type based on the attribute type. cls = value.__class__.__name__ params = None if cls == "Text": typ = PropertyDataTypes.string elif cls == "Integer": typ = PropertyDataTypes.integer elif cls == "Float": typ = PropertyDataTypes.float elif cls == "Boolean": typ = PropertyDataTypes.boolean elif cls == "Enumeration": typ = PropertyDataTypes.enum params = value.enumClass else: continue field = PropertyGridField(typ, "Properties", key, params) fields[key] = field if isinstance(self.value, ChangeNotifier): items = getmembers(self.value.__class__, lambda x: isinstance(x, NotifierProperty)) for key, value in items: if value.dataType is not None: fields[key] = PropertyGridField(value.dataType, value.section, key, value.dataTypeParam) if self.fields: # note that fields specified in XML override anything determined # automatically. for field in self.fields: fields[field.field] = field # filter out hidden fields. for key, field in fields.items(): if field.isHidden: del fields[key] return fields def onGetChildren(self, parser): """ Called once our children have been parsed, make a list of items """ self._buildFields() def _buildFields(self, _obj=None, _prop=None): """ Build fields on systems that don't have the PropertyGrid widget using standard Guide widgets. """ fields = self._getFields() parser = self.getParser() if self._scrollFrame is not None: self._scrollFrame.destroy() self._scrollFrame = parser.getWidgetClass("ScrollFrame")(parent=self, width="100%", height="100%") stack = parser.getWidgetClass("StackFrame")(parent=self._scrollFrame, width="100%", height="100%") sections = {} self._currentFields = {} for name, field in fields.items(): try: section = sections[field.section] except KeyError: sections[field.section] = [] section = sections[field.section] if field.dataType == PropertyDataTypes.enum: field.names, field.values = field.getEnumNamesAndValues() value = getattr(self.value, name, field.names[0]) else: value = getattr(self.value, name, None) section.append((field.priority, field.field, value, field)) self._currentFields[field.field] = field sections = sorted(sections.iteritems()) for sectionName, sectionItems in sections: sectionItems.sort(reverse=True) group = parser.getWidgetClass("GroupFrame")(parent=stack, text=sectionName, width="100%") grid = parser.getWidgetClass("GridFrame")(parent=group, width="100%") for _priority, _name, value, field in sectionItems: row = parser.getWidgetClass("GridRow")(parent=grid, width="100%") labelCell = parser.getWidgetClass("GridCell")(parent=row, width="50%") parser.getWidgetClass("Label")(parent=labelCell, text=field.field) valueCell = parser.getWidgetClass("GridCell")(parent=row, width="50%") controlCls = self.dataTypeMap[field.dataType] data = BoundData(field.field, self) if field.dataType == PropertyDataTypes.integer: control = controlCls(parent=valueCell, width="50%", value=data, dataContext=self._value) elif field.dataType == PropertyDataTypes.float: control = controlCls(parent=valueCell, width="50%", value=data, decimalPlaces=2, dataContext=self._value) elif field.dataType == PropertyDataTypes.enum: control = controlCls(parent=valueCell, width="50%", text=data, items=field.names, dataContext=self._value) else: control = controlCls(parent=valueCell, width="50%", text=data, dataContext=self._value) data.setWidget(control) self._scrollFrame.initialize(self.getParser()) class Tree(Widget): """ Displays a tree of items which can be selected. """ root = DataAccessor("_root") selection = DataAccessor("_selection") template = DataAccessor("_template") def __init__(self, root=None, selection=None, template=None, onSelect=None, onDoubleClick=None, onExpand=None, onCollapse=None, **attribs): Widget.__init__(self, **attribs) self.root = root self.selection = selection self.template = template self._onSelect = EventHandler() if onSelect is not None: self.listen("onSelect", onSelect) self._onDoubleClick = EventHandler() if onDoubleClick is not None: self.listen("onDoubleClick", onDoubleClick) self._onExpand = EventHandler() if onExpand is not None: self.listen("onExpand", onExpand) self._onCollapse = EventHandler() if onCollapse is not None: self.listen("onCollapse", onCollapse) def onSelect(self): """ An item was selected """ self._onSelect(self, self.selection) def onDoubleClick(self): """ An item was double clicked """ self._onDoubleClick(self, self.selection) def onExpand(self, item): """ An item was expanded """ self._onExpand(self, item) def onCollapse(self, item): """ An item was collapsed """ self._onCollapse(self, item) def onGetChildren(self, parser): """ Called once our children have been parsed, make a list of items """ Widget.onGetChildren(self, parser) for child in self.children: if isinstance(child, TreeItem): self.root = child break class TreeItem(Widget): """ An item in a tree. """ field = DataAccessor("_field", strstrip) items = DataAccessor("_items", listOrNone) image = DataAccessor("_image", default=None) imageFile = DataAccessor("_imageFile", strstrip, None) def __init__(self, field=None, items=None, image=None, imageFile=None, **attribs): Widget.__init__(self, **attribs) self.field = field self.items = items or [] self.image = image self.imageFile = imageFile def __iter__(self): """ We are iterable """ return iter(self.children) def __getitem__(self, index): """ Might as well support this too """ return self.children[index] def __str__(self): """ We can be stringified """ return str(self.text) def onGetChildren(self, parser): """ Called once our children have been parsed, make a list of items """ Widget.onGetChildren(self, parser) for child in self.children[:]: if isinstance(child, TreeItem): self.items += [child] class RenderFrame(Widget): """ A 3D Render Frame. """ cameraPosition = DataAccessor("_cameraPosition", makeVector, (0, 0, 0)) cameraOrientation = DataAccessor("_cameraOrientation", makeQuaternion, (1, 0, 0, 0)) cameraTarget = DataAccessor("_cameraTarget", makeVector, (0, 0, 0)) renderMode = DataAccessor("_renderMode", strstrip, "solid") backgroundColor = DataAccessor("_backgroundColor", makeColor, (0, 0, 0)) mouseMove = DataAccessor("_mouseMove", boolify, True) cameraMode = DataAccessor("_cameraMode", strstrip, "orbit") cameraController = DataAccessor("_cameraController", strstrip, "mv3d.util.camera.DefaultCameraController") orbitDist = DataAccessor("_orbitDist", floatOrNone, 1.0) lockCamera = DataAccessor("_lockCamera", boolify, False) lockSelection = DataAccessor("_lockSelection", boolify, False) items = DataAccessor("_items") selection = DataAccessor("_selection") showGrid = DataAccessor("_showGrid", boolify, False) space = DataAccessor("_space") nodeOffset = (0, 0, 10000) sceneNode = None def __init__(self, cameraPosition=(0, 0, 0), cameraOrientation=(1, 0, 0, 0), cameraTarget=(0, 0, 0), renderMode="solid", backgroundColor=(0, 0, 0), mouseMove=True, orbitDist=10, onMousePick=None, items=None, selection=None, onDragItem=None, cameraMode="orbit", lockCamera=False, cameraController="mv3d.util.camera.DefaultCameraController", lockSelection=False, onBeginDrag=None, onEndDrag=None, renderer=None, ownScene=True, showGrid=False, space=None, **attribs): Widget.__init__(self, **attribs) self.cameraController = cameraController self.camera = getClass(self.cameraController)() self.cameraPosition = cameraPosition self.cameraOrientation = Quaternion(cameraOrientation) self.cameraTarget = cameraTarget self.renderMode = renderMode self.backgroundColor = backgroundColor self.mouseMove = mouseMove self.orbitDist = orbitDist self.items = items or [] self.selection = selection self.cameraMode = cameraMode self.lockCamera = lockCamera self.lockSelection = lockSelection self.renderer = renderer self.ownScene = ownScene self.showGrid = showGrid self.space = space self._onMousePick = EventHandler() if onMousePick is not None: self.listen("onMousePick", onMousePick) self._onDragItem = EventHandler() if onDragItem is not None: self.listen("onDragItem", onDragItem) self._onBeginDrag = EventHandler() if onBeginDrag is not None: self.listen("onBeginDrag", onBeginDrag) self._onEndDrag = EventHandler() if onEndDrag is not None: self.listen("onEndDrag", onEndDrag) def onMousePick(self, item): """ Called when the user picks an object with the mouse. """ self._onMousePick(self, item) def onDragItem(self, delta): """ Called when the user drags an item with the left mouse button. """ self._onDragItem(self, delta) def onBeginDrag(self): """ Called when the user begins dragging an item with the left mouse button. """ self._onBeginDrag(self) def onEndDrag(self): """ Called when the user stops dragging an item with the left mouse button. """ self._onEndDrag(self) class MenuBar(Widget): """ A menu bar """ class Menu(Widget): """ A menu """ class MenuItem(Widget): """ A menu item """ itemType = DataAccessor("_itemType", enumeration(MenuItemType, MenuItemType.normal), False) selected = DataAccessor("_selected", boolify, False) subMenu = DataAccessor("_subMenu", default=None) image = DataAccessor("_image") def __init__(self, onClick=None, itemType="normal", selected=None, subMenu=None, image=None, **attribs): Widget.__init__(self, **attribs) self.itemType = itemType self.selected = selected self.subMenu = subMenu self.image = image self._onClick = EventHandler() if onClick is not None: self.listen("onClick", onClick) def onClick(self): """ Called when the menu item is clicked """ self._onClick(self) def enable(self): """ Enables the menu item """ #TODO: wx.MenuItems can't be enabled/disabled until they're appended # to a Menu. Check for this. self.window.Enable(True) def disable(self): """ Disables the menu item """ #TODO: wx.MenuItems can't be enabled/disabled until they're appended # to a Menu. Check for this. self.window.Enable(False) class Toolbar(Widget): """ A toolbar """ location = DataAccessor("_location", coerce=enumeration(DockLocations, DockLocations.top)) position = DataAccessor("_position", intOrNone, 0) layer = DataAccessor("_layer", intOrNone, 0) hasGripper = DataAccessor("_hasGripper", boolify, False) hasBorder = DataAccessor("_hasBorder", boolify, False) dockable = DataAccessor("_dockable", boolify, True) floatable = DataAccessor("_floatable", boolify, True) movable = DataAccessor("_movable", boolify, True) showText = DataAccessor("_showText", boolify, False) showOverflow = DataAccessor("_showOverflow", boolify, False) direction = DataAccessor("_direction", enumeration(Direction, Direction.horizontal)) textLocation = DataAccessor("_textLocation", enumeration( ToolbarTextLocations, ToolbarTextLocations.bottom)) def __init__(self, location=DockLocations.top, position=0, hasGripper=False, hasBorder=False, dockable=True, floatable=True, movable=True, showText=False, showOverflow=False, direction=Direction.horizontal, textLocation=ToolbarTextLocations.bottom, layer=0, **attribs): Widget.__init__(self, **attribs) self.location = location self.position = position self.hasGripper = hasGripper self.hasBorder = hasBorder self.dockable = dockable self.floatable = floatable self.movable = movable self.showText = showText self.showOverflow = showOverflow self.direction = direction self.textLocation = textLocation self.layer = layer class ToolbarItem(Widget): """ A toolbar """ type = DataAccessor("_type", coerce=enumeration(ToolbarItemType, ToolbarItemType.button)) image = DataAccessor("_image") checked = DataAccessor("_checked", bool) hoverImage = DataAccessor("_hoverImage", default=None) pressedImage = DataAccessor("_pressedImage", default=None) dropDownMenu = DataAccessor("_dropDownMenu", default=None) def __init__(self, type=None, image=None, hoverImage=None, pressedImage=None, checked=False, dropDownMenu=None, onClick=None, **attribs): Widget.__init__(self, **attribs) self.type = type self.image = image self.hoverImage = hoverImage self.pressedImage = pressedImage self.checked = checked self.dropDownMenu = dropDownMenu self._onClick = EventHandler() if onClick is not None: self.listen("onClick", onClick) def click(self): """ Send a click event. """ self._onClick(self) class Image(Widget): """ A static image """ image = DataAccessor("_image", default=None) imageFile = DataAccessor("_imageFile", strstrip, None) def __init__(self, image=None, imageFile=None, **attribs): Widget.__init__(self, **attribs) self.image = image self.imageFile = imageFile class DockableFrame(Widget): """ A frame that is a child window and can be docked to sides and top/bottom of the parent. """ location = DataAccessor("_location", enumeration(DockLocations, DockLocations.left)) position = DataAccessor("_position", intOrNone, 0) hasCloseButton = DataAccessor("_hasCloseButton", boolify, True) hasMaximizeButton = DataAccessor("_hasMaximizeButton", boolify, False) hasPinButton = DataAccessor("_hasPinButton", boolify, False) hasGripper = DataAccessor("_hasGripper", boolify, False) hasBorder = DataAccessor("_hasBorder", boolify, False) dockable = DataAccessor("_dockable", boolify, True) floatable = DataAccessor("_floatable", boolify, True) movable = DataAccessor("_movable", boolify, True) toolbar = DataAccessor("_toolbar", default=None) hasTitleBar = DataAccessor("_hasTitleBar", boolify, True) hasMinimizeButton = DataAccessor("_hasMinimizeBUtton", boolify, False) def __init__(self, location="left", position=0, hasCloseButton=True, hasMaximizeButton=False, hasPinButton=False, hasGripper=True, hasBorder=True, dockable=True, floatable=True, movable=True, hasTitleBar=True, onClose=None, toolbar=tuple(), hasMinimizeButton=False, **attribs): Widget.__init__(self, **attribs) self.location = location self.position = position self.hasCloseButton = hasCloseButton self.hasMaximizeButton = hasMaximizeButton self.hasPinButton = hasPinButton self.hasGripper = hasGripper self.hasBorder = hasBorder self.dockable = dockable self.floatable = floatable self.movable = movable self.toolbar = toolbar self.hasTitleBar = hasTitleBar self.hasMinimizeButton = hasMinimizeButton self._onClose = EventHandler() if onClose is not None: self.listen("onClose", onClose) def findChild(self, value, attr="name"): """ Recursively search until we find the child or return None if not found. If attr is specified, the child will be selected by that attribute matching the specified value. """ if isinstance(self.toolbar, list): for child in self.toolbar or []: if hasattr(child, attr) and getattr(child, attr) == value: return child result = child.findChild(value, attr) if result is not None: return result return Widget.findChild(self, value, attr) def onClose(self): """ The frame has been closed """ self._onClose(self) class TabFrame(Widget): """ A frame that holds one or more tabs. """ selection = DataAccessor("_selection") def __init__(self, selection=None, **attribs): Widget.__init__(self, **attribs) self.selection = selection class Tab(Widget): """ A tab in a TabFrame """ def __init__(self, onClose=None, **attribs): Widget.__init__(self, **attribs) self._onClose = EventHandler() if onClose is not None: self.listen("onClose", onClose) def onClose(self): """ The tab has been closed """ self._onClose(self) class ExpandFrame(Widget): """ A frame that can expand or contract """ expanded = DataAccessor("_expanded", boolify, False) def __init__(self, expanded=False, **attribs): Widget.__init__(self, **attribs) self.expanded = expanded class FileSelector(Widget): """ Opens up a file selector window and selects a file. """ directory = DataAccessor("_directory", strstrip, os.getcwd()) pattern = DataAccessor("_pattern", strstrip, "*.*") fileName = DataAccessor("_fileName", strstrip, "") operation = DataAccessor("_operation", strstrip, "load") selectMultiple = DataAccessor("_selectMultiple", boolify, False) selection = DataAccessor("_selection") def __init__(self, directory=None, pattern=None, fileName=None, operation="load", selectMultiple=False, selection=None, onSelect=None, onCancel=None, **attribs): Widget.__init__(self, **attribs) if directory is not None: self.directory = directory if pattern is not None: self.pattern = pattern if fileName is not None: self.fileName = fileName if selection is not None: self.selection = selection self.selectMultiple = selectMultiple self.operation = operation self._onSelect = EventHandler() if onSelect is not None: self.listen("onSelect", onSelect) self._onCancel = EventHandler() if onCancel is not None: self.listen("onCancel", onCancel) def onSelect(self): """ The file was selected """ self._onSelect(self, self.selection) def onCancel(self): """ The file selection was cancelled """ self._onCancel(self, None) class DialogButton(Enum): """ Enumeration of message dialog button flags. """ ok = 1 cancel = 2 yes = 4 no = 8 yesAndNo = 12 help = 16 #@ReservedAssignment class DialogIcon(Enum): """ Enumeration of message dialog icons. """ info = 0 warning = 1 exclaimation = 2 error = 3 question = 4 class MessageDialog(Widget): """ Opens up a message dialog box. """ title = DataAccessor("_title", strstrip, "Message For You!") buttons = DataAccessor("_buttons", enumeration(DialogButton, DialogButton.ok)) iconType = DataAccessor("_iconType", enumeration(DialogIcon, DialogIcon.info)) image = DataAccessor("_image") def __init__(self, title="", buttons=DialogButton.ok, image=None, iconType=DialogIcon.info, onDismiss=None, onOk=None, onCancel=None, onYes=None, onNo=None, onHelp=None, **attribs): Widget.__init__(self, **attribs) self.title = title self.buttons = buttons self.image = image self.iconType = iconType self._onDismiss = EventHandler() if onDismiss is not None: self.listen("onDismiss", onDismiss) self._onOk = EventHandler() if onOk is not None: self.listen("onOk", onOk) self._onCancel = EventHandler() if onCancel is not None: self.listen("onCancel", onCancel) self._onYes = EventHandler() if onYes is not None: self.listen("onYes", onYes) self._onNo = EventHandler() if onNo is not None: self.listen("onNo", onNo) self._onHelp = EventHandler() if onHelp is not None: self.listen("onHelp", onHelp) def onDismiss(self, result): """ The dialog was dismissed. """ self._onDismiss(self, result) def onOk(self): """ The user hit ok. """ self._onOk(self) def onCancel(self): """ The user hit ok. """ self._onCancel(self) def onYes(self): """ The user hit ok. """ self._onYes(self) def onNo(self): """ The user hit ok. """ self._onNo(self) def onHelp(self): """ The user hit ok. """ self._onHelp(self) class PopupWindow(Widget): """ A popup window (just a frame pretty much) """ isTransient = DataAccessor("_isTransient", boolify) def __init__(self, isTransient=False, onDismiss=None, **attribs): Widget.__init__(self, **attribs) self.isTransient = isTransient self._onDismiss = EventHandler() if onDismiss is not None: self.listen("onDismiss", onDismiss) def onDismiss(self): """ The popup was dismissed. """ self._onDismiss(self) class Window(Widget): """ A window! """ menubar = DataAccessor("_menubar", default=None) toolbar = DataAccessor("_toolbar", default=None) def __init__(self, menubar=None, toolbar=tuple(), onClose=None, **attribs): Widget.__init__(self, **attribs) self.menubar = menubar self.toolbar = toolbar self._onClose = EventHandler() if onClose is not None: self.listen("onClose", onClose) def findChild(self, value, attr="name"): """ Recursively search until we find the child or return None if not found. If attr is specified, the child will be selected by that attribute matching the specified value. """ for child in self.toolbar or []: if hasattr(child, attr) and getattr(child, attr) == value: return child result = child.findChild(value, attr) if result is not None: return result return Widget.findChild(self, value, attr) def onClose(self, _event=None): """ Called when the close button is hit. """ self._onClose(self) class Template(object): """ A template is a collection of widgets """ def __init__(self, tag): self.tag = tag class TemplateFactory(object): """ A factory which can produce instances of a template """ def __init__(self, parser, template, params=None): self.parser = parser self.template = template self.params = params def produce(self, dataContext, parent): """ Actually construct the widgets here. """ requireWidget = [] for param in self.params or []: key, value = param.split("=") value = self.parser.parseData(value, dataContext, parent, requireWidget) if key == "dataContext": dataContext = value for require in requireWidget: require.setWidget(parent) widgets = [] for child in self.template.tag.getchildren(): widgets.append(self.parser.parseWidget(child, dataContext, parent=parent)) return widgets class Style(object): """ Styles define the look of widgets """ def __init__(self): self.defaults = {} self.styles = {} def get(self, elementName, attribName): """ Returns the style for the specified element / attribute """ if self.styles.has_key(elementName): style = self.styles[elementName] if style.has_key(attribName): return style[attribName] return self.defaults.get(attribName) class PropertyListener(object): """ This object can be used to remove a property listener. """ def __init__(self, obj, property, callback): self.obj = obj self.property = property self.callback = callback def remove(self): """ Remove the propety listener. """ self.obj.removePropertyListener(self.property, self.callback) def callback(self): """ Call it back! """ try: res = self.callback(self.obj, self.property) if isinstance(res, Deferred): if hasattr(self.obj, "errorHandler") and callable(self.obj.errorHandler): res.addErrback(self.obj.errorHandler) elif hasattr(self.obj, "parser"): res.addErrback(self.obj.parser.errorHandler) else: res.addErrback(deferr) except: if hasattr(self.obj, "errorHandler") and callable(self.obj.errorHandler): self.obj.errorHandler() elif hasattr(self.obj, "parser"): self.obj.parser.errorHandler() else: deferr() raise class Data(object): """ Holds a bit of data or a means of getting it. """ widget = None def __init__(self, data): self.data = data self.callbacks = [] def listen(self, callback): """ Listen for changes on this data. """ self.callbacks.append(callback) def stopListening(self, callback): """ Stop listening for changes on this data. """ self.callbacks.remove(callback) def getData(self, widget=None): """ Returns the data """ return self.data def callback(self): """ Call back all the listeners """ for callback in self.callbacks[:]: try: res = callback(self, self.data) if isinstance(res, Deferred): if hasattr(self.data, "errorHandler") and callable(self.data.errorHandler): res.addErrback(self.data.errorHandler) else: res.addErrback(deferr) except: if hasattr(self.data, "errorHandler") and callable(self.data.errorHandler): self.data.errorHandler() else: deferr() def setData(self, data): """ Sets the data and calls callbacks listening for data changes. """ self.data = data self.callback() class BoundData(Data): """ Holds a databinding """ def __init__(self, attrib, hackTempWidget=None): self.widget = hackTempWidget self.attrib = attrib self.callbacks = [] self.listeners = [] def setWidget(self, widget): """ Set the widget this is attached to """ self.widget = widget if self.attrib == "self": return obj = self.widget.dataContext for attr in self.attrib.split(".")[:-1]: if hasattr(obj, "addPropertyListener"): self.listeners.append( obj.addPropertyListener(attr, self._reset)) if not hasattr(obj, attr): break obj = getattr(obj, attr) def _reset(self, obj, prop): """ Reset all the callbacks """ callbacks = self.callbacks[:] self.callbacks = [] for listener in self.listeners: listener.remove() self.listeners = [] for callback in callbacks[:]: self.listen(callback) try: res = callback(obj, prop) if isinstance(res, Deferred): if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): res.addErrback(self.widget.dataContext.errorHandler) else: res.addErrback(self.widget.getParser().errorHandler) except: if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): self.widget.dataContext.errorHandler() else: self.widget.getParser().errorHandler() if self.attrib == "self": return obj = self.widget.dataContext for attr in self.attrib.split(".")[:-1]: if hasattr(obj, "addPropertyListener"): self.listeners.append( obj.addPropertyListener(attr, self._reset)) if not hasattr(obj, attr): break obj = getattr(obj, attr) def listen(self, callback): """ Listen for changes on this data. """ Data.listen(self, callback) if self.attrib == "self": return obj = self.widget.dataContext for attr in self.attrib.split("."): if hasattr(obj, "addPropertyListener"): self.listeners.append(obj.addPropertyListener(attr, callback)) try: obj = getattr(obj, attr) except AttributeError: break def stopListening(self, callback): """ Stop listening for changes on this data. """ Data.stopListening(self, callback) if self.attrib == "self": return obj = self.widget.dataContext for attr in self.attrib.split("."): if hasattr(obj, "removePropertyListener"): obj.removePropertyListener(attr, callback) try: obj = getattr(obj, attr) except AttributeError: break def getData(self, widget=None): """ data getter """ if self.widget is None: self.widget = widget if self.attrib == "self": return self.widget.dataContext obj = self.widget.dataContext try: for attrib in self.attrib.split(".")[:-1]: obj = getattr(obj, attrib) return getattr(obj, self.attrib.split(".")[-1]) except AttributeError: return None def setData(self, value): """ data setter """ if self.attrib == "self": self.widget.dataContext = value # eek? return obj = self.widget.dataContext attrib = self.attrib if obj is None: return try: for attrib in self.attrib.split(".")[:-1]: obj = getattr(obj, attrib) if obj is None: return except AttributeError: return attrib = self.attrib.split(".")[-1] if hasattr(obj, attrib) and getattr(obj, attrib) == value: return setattr(obj, attrib, value) class BoundMethodData(Data): """ Holds a databinding pointer to a function. """ def __init__(self, funcName, args): self.funcName = funcName self.args = args self.callbacks = [] self.listeners = [] def setWidget(self, widget): """ Set the widget this is attached to """ self.widget = widget for arg in self.args: arg.listen(self._runCallbacks) obj = self.widget.dataContext for attr in self.funcName.getData().split(".")[:-1]: if hasattr(obj, "addPropertyListener"): self.listeners.append( obj.addPropertyListener(attr, self._reset)) if not hasattr(obj, attr): break obj = getattr(obj, attr) def _runCallbacks(self, obj, prop): """ Call back everyone. """ for callback in self.callbacks[:]: try: res = callback(obj, prop) if isinstance(res, Deferred): if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): res.addErrback(self.widget.dataContext.errorHandler) else: res.addErrback(self.widget.getParser().errorHandler) except: if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): self.widget.dataContext.errorHandler() else: self.widget.getParser().errorHandler() def _reset(self, _obj, _prop): """ Reset all the callbacks """ callbacks = self.callbacks[:] self.callbacks = [] for listener in self.listeners: listener.remove() self.listeners = [] for callback in callbacks: self.listen(callback) for listener in self.listeners: listener.callback() def _fixArgs(self): """ Gather and fix up all the args """ args = [] for arg in self.args: argData = arg.getData() if isinstance(argData, (str, unicode)): argData = argData.strip() if "." in argData and len(argData.split(".")) == 2: try: argData = float(argData) except ValueError: pass if argData.lower() == "true": argData = True elif argData.lower() == "true": argData = False elif argData.isdigit(): argData = int(argData) args.append(argData) return args def listen(self, callback): """ Listen for changes on this data. """ Data.listen(self, callback) obj = self.widget.dataContext for attr in self.funcName.getData().split("."): if hasattr(obj, "addPropertyListener"): self.listeners.append(obj.addPropertyListener(attr, callback)) try: obj = getattr(obj, attr) except AttributeError: break def stopListening(self, callback): """ Stop listening for changes on this data. """ Data.stopListening(self, callback) obj = self.widget.dataContext for attr in self.funcName.getData().split("."): if hasattr(obj, "removePropertyListener"): obj.removePropertyListener(attr, callback) try: obj = getattr(obj, attr) except AttributeError: break def getData(self, widget=None): """ data getter """ if self.widget is None: self.widget = widget args = self._fixArgs() obj = self.widget.dataContext funcPath = self.funcName.getData().split(".") try: for attrib in funcPath[:-1]: obj = getattr(obj, attrib) return getattr(obj, funcPath[-1])(*args) except AttributeError: return def setData(self, value): """ data setter doesn't work """ raise ValueError("Can't set BoundMethodData.") class BoundEvent(Data): """ Holds a databinding to an event """ def __init__(self, funcName): self.funcName = funcName self.callbacks = [] self.listeners = [] def __str__(self): """ Stringify """ return "BoundEvent for %s" % self.funcName def setWidget(self, widget): """ Set the widget this is bound to """ self.widget = widget obj = self.widget.dataContext for attr in self.funcName.split(".")[:-1]: if hasattr(obj, "addPropertyListener"): self.listeners.append( obj.addPropertyListener(attr, self._reset)) if not hasattr(obj, attr): break obj = getattr(obj, attr) def _reset(self, obj, prop): """ Reset all the callbacks """ callbacks = self.callbacks[:] self.callbacks = [] for listener in self.listeners: listener.remove() self.listeners = [] for callback in callbacks[:]: self.listen(callback) try: res = callback(obj, prop) if isinstance(res, Deferred): if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): res.addErrback(self.widget.dataContext.errorHandler) else: res.addErrback(self.widget.getParser().errorHandler) except: if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): self.widget.dataContext.errorHandler() else: self.widget.getParser().errorHandler() def listen(self, callback): """ Listen for changes on this data. """ Data.listen(self, callback) obj = self.widget.dataContext for attr in self.funcName.split("."): if hasattr(obj, "addPropertyListener"): self.listeners.append(obj.addPropertyListener(attr, callback)) try: obj = getattr(obj, attr) except AttributeError: break def stopListening(self, callback): """ Stop listening for changes on this data. """ Data.stopListening(self, callback) obj = self.widget.dataContext for attr in self.funcName.split("."): if hasattr(obj, "removePropertyListener"): obj.removePropertyListener(attr, callback) try: obj = getattr(obj, attr) except AttributeError: continue def __call__(self, *args, **kwargs): """ data getter """ obj = self.widget.dataContext funcPath = self.funcName.split(".") try: for attrib in funcPath[:-1]: obj = getattr(obj, attrib) func = getattr(obj, funcPath[-1]) except AttributeError: return return func(*args, **kwargs) class DataConverter(Data): """ Subclass this to be able to convert your data. """ def __init__(self, args): self.args = args self.callbacks = [] def setWidget(self, widget): """ Set the widget """ self.widget = widget for arg in self.args: arg.listen(self._dataChanged) def _dataChanged(self, _obj, _prop): """ Set the data """ result = self.getData() for callback in self.callbacks[:]: try: res = callback(self, result) if isinstance(res, Deferred): if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): res.addErrback(self.widget.dataContext.errorHandler) else: res.addErrback(self.widget.getParser().errorHandler) except: if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): self.widget.dataContext.errorHandler() else: self.widget.getParser().errorHandler() def _reset(self, _obj, _prop): """ Reset all the callbacks """ callbacks = self.callbacks[:] self.callbacks = [] for listener in self.listeners: listener.remove() self.listeners = [] for callback in callbacks: self.listen(callback) self.listeners[-1].callback() def getData(self): """ Fix up args and convert data """ return self.convertTo(*self.args) def setData(self, value): """ Fix up args and convert data and call callbacks """ result = self.convertFrom(value, *self.args) for callback in self.callbacks[:]: try: res = callback(self, result) if isinstance(res, Deferred): if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): res.addErrback(self.widget.dataContext.errorHandler) else: res.addErrback(self.widget.getParser().errorHandler) except: if hasattr(self.widget.dataContext, "errorHandler") and ( callable(self.widget.dataContext.errorHandler)): self.widget.dataContext.errorHandler() else: self.widget.getParser().errorHandler() return result class EventHandler(object): """ Holds callbacks and such for events """ def __init__(self): self.listeners = [] def listen(self, callback): """ Add a new callback """ if not callback in self.listeners: self.listeners.append(callback) def stopListening(self, callback): """ Remove a previously registered callback """ self.listeners.remove(callback) def __call__(self, caller, data=None): """ Call all the listeners. """ for listener in self.listeners[:]: try: res = listener(caller, data) if isinstance(res, Deferred): if hasattr(caller, "errorHandler") and callable(caller.errorHandler): res.addErrback(caller.errorHandler) elif hasattr(data, "errorHandler") and callable(data.errorHandler): res.addErrback(data.errorHandler) elif hasattr(caller, "dataContext") and hasattr( caller.dataContext, "errorHandler") and ( caller.dataContext.errorHandler is not None): res.addErrback(caller.dataContext.errorHandler) elif hasattr(caller, "getParser"): res.addErrback(caller.getParser().errorHandler) else: res.addErrback(deferr) except TypeError, exc: if "object is not callable" in str(exc): raise TypeError("Event required, %s found! (%s)" % (listener.__class__.__name__, listener)) if hasattr(caller, "errorHandler") and callable(caller.errorHandler): caller.errorHandler() elif hasattr(data, "errorHandler") and callable(data.errorHandler): data.errorHandler() elif hasattr(caller, "dataContext") and hasattr( caller.dataContext, "errorHandler") and ( caller.dataContext.errorHandler is not None): caller.dataContext.errorHandler() elif hasattr(caller, "getParser"): caller.getParser().errorHandler() else: deferr() except: if hasattr(caller, "errorHandler") and callable(caller.errorHandler): caller.errorHandler() elif hasattr(data, "errorHandler") and callable(data.errorHandler): data.errorHandler() elif hasattr(caller, "dataContext") and hasattr( caller.dataContext, "errorHandler") and ( caller.dataContext.errorHandler is not None): caller.dataContext.errorHandler() elif hasattr(caller, "getParser"): caller.getParser().errorHandler() else: deferr() class Parser(object): """ The parser loads the XML files """ baseDir = os.path.abspath(os.path.join( os.path.dirname(mv3d.__file__), "..")) width = 640 height = 480 clipboard = None errorHandler = deferr def __init__(self, library=None, baseDir=None): self.library = library or globals() self.templates = {} self.styles = {} self.customControls = {} self.dataContext = None self.baseDir = baseDir or self.baseDir def _getWidgetClass(self, widgetName): """ Private method for getting a widget by first checking custom controls. """ return self.getWidgetClass(widgetName) def load(self, loadFrom, dataContext=None, conductor=None, parent=None, template=None): """ Loads the widgets and returns the top level widget. loadFrom can be a string (which is assumed to be a file name), an asset ID (in which case the conductor must be specified), or a file-like object. If parent is specified, then loaded widgets are parented to it. """ self.dataContext = dataContext if parent is None: parent = self if isinstance(loadFrom, (str, unicode)) and not os.path.exists( loadFrom): if os.path.exists(os.path.join(self.baseDir, loadFrom)): loadFrom = os.path.join(self.baseDir, loadFrom) guide = parse(loadFrom).getroot() if guide.attrib.has_key("dataSource") and dataContext is None: conGen = getClass(guide.attrib["dataSource"]) dataContext = conGen(conductor, self) widget = None for element in guide.getchildren(): if element.tag == "Template": name = element.get("name") self.templates[name] = self.parseTemplate(element, dataContext) elif element.tag == "Style": name = element.get("name") self.styles[name] = self.parseStyle(element, dataContext) elif element.tag == "ImportControls": schema = element.get("schema") module = element.get("module") self.customControls[schema] = __import__(module, fromlist="*") elif template is None: assert widget is None widget = self.parseWidget(element, dataContext, parent=parent) if template is not None: return TemplateFactory(self, self.templates[template]).produce( dataContext, parent) return widget def parseWidget(self, root, dataContext, style=None, extraAttribs=None, parent=None): """ Parse a widget out of XML """ if ":" in root.tag: schema, widgetName = root.tag.split("}") schema = schema.strip("{") customClass = getattr(self.customControls[schema], widgetName) cls = self.getWidgetClass("CustomControlFrame") else: cls = self.getWidgetClass(root.tag) customClass = None requireWidget = [] style, attribs = self._parseAttribs(root, dataContext, style, parent, requireWidget) attribs["parent"] = parent attribs.update(extraAttribs or {}) try: del attribs["style"] except KeyError: pass if customClass is None: widget = cls(**attribs) else: widget = cls(customClass, **attribs) if isinstance(widget, CustomControl): self.setupCustomControl(widget) self._parseChildren(widget, root, widget.dataContext, style) for require in requireWidget: require.setWidget(widget) widget.onGetChildren(self) widget.onLoad() return widget def _parseChildren(self, widget, root, dataContext, style, extraAttribs=None): """ Given our tag, parse all child tags and then notify the widget """ for child in root.getchildren(): if not child.tag.startswith(root.tag + "."): childWidget = self.parseWidget(child, dataContext, style, extraAttribs, widget) if childWidget is None: continue widget.children.append(childWidget) def setupCustomControl(self, widget): """ Override this function when creating a new UI integration. It should handle adding any required attributes to the custom control instance that is given to it. """ def getWidgetClass(self, name): """ Override this function when creating a new UI integration. It should return a Widget subclass for the given widget type. """ return self.library[name] def _parseAttribs(self, root, dataContext, style=None, parent=None, requireWidget=None): """ Create a dictionary of attributes which will be passed to a newly created Widget """ attribs = {} if isinstance(dataContext, Data): attribs["dataContext"] = dataContext else: attribs["dataContext"] = Data(dataContext) style = root.get("style", style) if style is not None and self.styles.has_key(style): theStyle = self.styles[style] attribs.update(theStyle.defaults) attribs.update(theStyle.styles.get(root.tag, {})) attribs.update(self.atttribsToData(root.attrib, attribs["dataContext"].getData(parent), parent, requireWidget)) if root.text is not None and len(root.text.strip()): attribs["text"] = self.parseData(root.text.strip(), attribs["dataContext"].getData(parent), parent, requireWidget) for child in root.getchildren(): if child.tag.startswith(root.tag + "."): attrib = child.tag.split(".")[-1] if len(child.getchildren()): template = Template(child) attribs[attrib] = TemplateFactory(self, template) else: attribs[attrib] = self.parseData(child.text, attribs["dataContext"].getData(parent), parent, requireWidget) return style, attribs def atttribsToData(self, attribs, dataContext, parent, requireWidget): """ Take the incoming attribute dict and return a new dict with Data instances. """ dataAttribs = {} for key, value in attribs.items(): reqWidget = requireWidget if key == "dataContext": reqWidget = [] dataAttribs[key] = self.parseData(value, dataContext, parent, reqWidget) if key == "dataContext" and isinstance(dataAttribs[key], Data): dataAttribs[key].setWidget(parent) return dataAttribs def parseData(self, value, dataContext, parent, requireWidget): """ Parse a bit of data and return a Data instance """ if value.strip().startswith("{") and value.strip().endswith("}"): binder = value.strip(" \n\r")[1:-1] data = binder.split(" ") method, what = data[0], " ".join(data[1:]) if method == "Bind": bound = BoundData(what, parent) requireWidget.append(bound) return bound elif method == "Event": bound = BoundEvent(what) requireWidget.append(bound) return bound elif method == "Method": callAndArgs = what.split(",") call = self.parseData(callAndArgs[0], dataContext, parent, requireWidget) args = [self.parseData(arg, dataContext, parent, requireWidget) for arg in callAndArgs[1:]] bound = BoundMethodData(call, args) requireWidget.append(bound) return bound elif method == "Convert": classAndArgs = what.split(",") class_ = self.parseData(classAndArgs[0], dataContext, parent, requireWidget) args = [self.parseData(arg, dataContext, parent, requireWidget) for arg in classAndArgs[1:]] modName = ".".join(class_.getData().split(".")[:-1]) className = class_.getData().split(".")[-1] mod = __import__(modName, fromlist=[className]) cls = getattr(mod, className) bound = cls(args) requireWidget.append(bound) return bound elif method == "Template": broken = what.split(",") if len(broken) == 1: return TemplateFactory(self, self.templates[broken[0]]) return TemplateFactory(self, self.templates[broken[0]], broken[1:]) elif method == "Image": method, data = what.split(":") return self.loadImage(self.parseData(method, dataContext, parent, requireWidget), self.parseData(data, dataContext, parent, requireWidget)) elif method == "EnumNames": # Useful for binding a combobox or listbox to enum names. enumClass = getClass(what) return Data(enumClass.keys()) return Data(value) def loadImage(self, method, data): """ Should return an image for the gui system in use. """ raise NotImplementedError("%s doesn't support loadImage" % self.__class__.__name__) def parseTemplate(self, root, _dataContext): """ Parse a template out of XML """ return Template(root) def parseStyle(self, root, dataContext): """ Parse a style out of XML """ style = Style() for element in root.getchildren(): if element.tag == "Any": for default in element.getchildren(): style.defaults[default.tag] = self.parseData( default.text.strip(), dataContext, None, None) else: attribs = {} for attrib in element.getchildren(): attribs[attrib.tag] = self.parseData(attrib.text.strip(), dataContext, None, None) style.styles[element.tag] = attribs return style def produceTemplate(self, templateName, dataContext, parent=None): """ Produce a template """ return TemplateFactory(self, self.templates[templateName]).produce( dataContext, parent or self) def getTemplate(self, templateName): """ Returns a TemplateFactory """ return TemplateFactory(self, self.templates[templateName]) def setClipboard(self, data): """ Add some data to the clipboard. This uses an internal clipboard, other parsers will need to override. """ self.clipboard = data def getClipboard(self): """ Returns whatever is in the clipboard. This uses an internal clipboard, other parsers will need to override. """ return self.clipboard def clearClipboard(self): """ Remove the data in the clipboard """ self.clipboard = None