Rewrote the XML <-> FreeCAD conversion on forms (model, form, types…). This commit crashes the latest release of FreeCAD when setting two linked properties with an expression

This commit is contained in:
Suzanne Soy 2021-04-30 13:07:03 +01:00
parent a663483c08
commit 90b5f9c269
8 changed files with 227 additions and 134 deletions

View File

@ -5,7 +5,7 @@ import PySide
from PySide import QtGui from PySide import QtGui
from PySide import QtCore from PySide import QtCore
import ExternalAppsList import XternalAppsList
import Embed import Embed
class AppCommand(): class AppCommand():
@ -14,7 +14,7 @@ class AppCommand():
def GetResources(self): def GetResources(self):
return { return {
'Pixmap': ExternalAppsList.apps[self.appName].Icon, 'Pixmap': XternalAppsList.apps[self.appName].Icon,
'Accel': "Shit+E", # E for Embed 'Accel': "Shit+E", # E for Embed
'MenuText': "Start " + self.appName, 'MenuText': "Start " + self.appName,
'ToolTip': "Start " + self.appName, 'ToolTip': "Start " + self.appName,
@ -29,4 +29,5 @@ class AppCommand():
return True return True
def createCommands(appName): def createCommands(appName):
Gui.addCommand('ExternalAppsOpen' + appName + 'Command', AppCommand(appName)) Gui.addCommand('XternalAppsOpen' + appName + 'Command', AppCommand(appName))

View File

@ -6,7 +6,7 @@ import re
from PySide import QtGui from PySide import QtGui
from PySide import QtCore from PySide import QtCore
import ExternalAppsList import XternalAppsList
from MyX11Utils import * from MyX11Utils import *
#class MyMdiSubWindow(QMdiSubWindow): #class MyMdiSubWindow(QMdiSubWindow):
@ -107,9 +107,9 @@ def try_pipe_lines(commandAndArguments):
return [] return []
# TODO: this is just a quick & dirty way to attach a field to the FreeCad object # TODO: this is just a quick & dirty way to attach a field to the FreeCad object
class ExternalApps(): class XternalApps():
def __init__(self): def __init__(self):
setattr(FreeCAD, 'ExternalApps', self) setattr(FreeCAD, 'XternalApps', self)
def deleted(widget): def deleted(widget):
"""Detect RuntimeError: Internal C++ object (PySide2.QtGui.QWindow) already deleted.""" """Detect RuntimeError: Internal C++ object (PySide2.QtGui.QWindow) already deleted."""
@ -122,7 +122,7 @@ def deleted(widget):
class ExternalAppInstance(QtCore.QObject): class ExternalAppInstance(QtCore.QObject):
def __init__(self, appName): def __init__(self, appName):
super(ExternalAppInstance, self).__init__() super(ExternalAppInstance, self).__init__()
self.app = ExternalAppsList.apps[appName] self.app = XternalAppsList.apps[appName]
# Start the application # Start the application
# TODO: popen_process shouldn't be exposed to in-document scripts, it would allow them to redirect output etc. # TODO: popen_process shouldn't be exposed to in-document scripts, it would allow them to redirect output etc.
print('Starting ' + ' '.join(self.app.start_command_and_args)) print('Starting ' + ' '.join(self.app.start_command_and_args))
@ -131,7 +131,7 @@ class ExternalAppInstance(QtCore.QObject):
self.initWaitForWindow() self.initWaitForWindow()
self.foundWindows = dict() self.foundWindows = dict()
self.closedWindows = dict() self.closedWindows = dict()
setattr(FreeCAD.ExternalApps, self.app.name, self) setattr(FreeCAD.XternalApps, self.app.name, self)
def initWaitForWindow(self): def initWaitForWindow(self):
self.TimeoutHasOccurred = False # for other scritps to know the status self.TimeoutHasOccurred = False # for other scritps to know the status

View File

@ -1,6 +1,6 @@
import sys import sys
import ExternalAppsList import XternalAppsList
import StealSplash import StealSplash
StealSplash.steal() StealSplash.steal()
@ -46,27 +46,30 @@ class XternalAppsWorkbench(Workbench):
"""Subclasses must implement the appName attribute""" """Subclasses must implement the appName attribute"""
global myIcon global myIcon
global XternalAppsWorkbench global XternalAppsWorkbench
global ExternalAppsList global XternalAppsList
def __init__(self): def __init__(self):
self.MenuText = "XternalApps: " + self.appName self.MenuText = "XternalApps: " + self.appName
self.ToolTip = "Embeds " + self.appName + " in FreeCAD" self.ToolTip = "Embeds " + self.appName + " in FreeCAD"
self.Icon = ExternalAppsList.apps[self.appName].Icon self.Icon = XternalAppsList.apps[self.appName].Icon
super(XternalAppsWorkbench, self).__init__() super(XternalAppsWorkbench, self).__init__()
def Initialize(self): def Initialize(self):
# Load commands # Load commands
import AppCommand import AppCommand
import ToolCommand import ToolCommand
import ReloadCommand
import Embed import Embed
Embed.ExternalApps() Embed.XternalApps()
AppCommand.createCommands(self.appName) AppCommand.createCommands(self.appName)
ToolCommand.createCommands(self.appName) ToolCommand.createCommands(self.appName)
ReloadCommand.createCommands(self.appName)
# List of commands for this workbench # List of commands for this workbench
self.list = ['ExternalAppsOpen' + self.appName + 'Command'] \ self.list = ['XternalAppsOpen' + self.appName + 'Command'] \
+ ['ExternalAppsTool' + self.appName + toolName + 'Command' + ['XternalAppsReload' + self.appName + 'Command'] \
for toolName in ExternalAppsList.apps[self.appName].Tools] + ['XternalAppsTool' + self.appName + toolName + 'Command'
for toolName in XternalAppsList.apps[self.appName].Tools]
# Create menus and toolbars # Create menus and toolbars
self.appendMenu("ExternalApplications", self.list) self.appendMenu("ExternalApplications", self.list)
@ -90,5 +93,5 @@ def addAppWorkbench(appName):
(XternalAppsWorkbench,), { 'appName': appName }) (XternalAppsWorkbench,), { 'appName': appName })
Gui.addWorkbench(workbenchClass()) Gui.addWorkbench(workbenchClass())
for appName in ExternalAppsList.apps: for appName in XternalAppsList.apps:
addAppWorkbench(appName) addAppWorkbench(appName)

View File

@ -6,7 +6,7 @@ import re
from PySide import QtGui from PySide import QtGui
from PySide import QtCore from PySide import QtCore
import ExternalAppsList import XternalAppsList
def x11stillAlive(windowId): def x11stillAlive(windowId):
try: try:

View File

@ -5,13 +5,13 @@ import PySide
from PySide import QtGui from PySide import QtGui
from PySide import QtCore from PySide import QtCore
import ExternalAppsList import XternalAppsList
import Embed import Embed
import XternalAppsParametricTool import XternalAppsParametricTool
class ToolCommand(): class ToolCommand():
def __init__(self, appName, toolName): def __init__(self, appName, toolName):
self.Tool = ExternalAppsList.apps[appName].Tools[toolName] self.Tool = XternalAppsList.apps[appName].Tools[toolName]
def GetResources(self): def GetResources(self):
return { return {
@ -29,5 +29,5 @@ class ToolCommand():
return FreeCAD.ActiveDocument is not None return FreeCAD.ActiveDocument is not None
def createCommands(appName): def createCommands(appName):
for toolName in ExternalAppsList.apps[appName].Tools: for toolName in XternalAppsList.apps[appName].Tools:
Gui.addCommand('ExternalAppsTool' + appName + toolName + 'Command', ToolCommand(appName, toolName)) Gui.addCommand('XternalAppsTool' + appName + toolName + 'Command', ToolCommand(appName, toolName))

View File

@ -7,8 +7,8 @@ def getSingletonFromXML(xml, path):
return elem return elem
ns={ ns={
# 'my':"http://github.com/jsmaniac/XternalApps/myTool", 'my':"http://github.com/jsmaniac/XternalApps/myTool",
'XternalApps':"http://github.com/jsmaniac/XternalApps", 'XternalApps':"http://github.com/jsmaniac/XternalApps/v1",
'xforms':"http://www.w3.org/2002/xforms", 'xforms':"http://www.w3.org/2002/xforms",
'xsd':"http://www.w3.org/2001/XMLSchema", 'xsd':"http://www.w3.org/2001/XMLSchema",
} }

View File

@ -1,14 +1,19 @@
import FreeCAD as App import FreeCAD as App
import FreeCADGui import FreeCADGui
from lxml import etree from lxml import etree
import ExternalAppsList import XternalAppsList
from ToolXML import * from ToolXML import *
from collections import namedtuple from collections import namedtuple
import re import re
from copy import deepcopy from copy import deepcopy
from collections import defaultdict from collections import defaultdict
import pprint
XFormsInput = namedtuple('XFormsInput', ['input', 'modelElementPath', 'type', 'maybeEnum', 'InputValueToModelValue', 'ModelValueToInputValue']) parser = etree.XMLParser(resolve_entities=False)
FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues'])
XFormsInput = namedtuple('XFormsInput', ['modelElementPath', 'label', 'simpleName', 'maybeEnum', 'groupName']) #'type', 'input', 'InputValueToModelValue', 'ModelValueToInputValue'
XFormsEnum = namedtuple('XFormsEnum', ['labels', 'values']) XFormsEnum = namedtuple('XFormsEnum', ['labels', 'values'])
InterpretedXML = namedtuple('InterpretedXML', ['xml', 'types', 'inputs']) # Parsed XML, dictionary(modelElementPath) -> type, dictionary(formElementPath) -> XFormsInput InterpretedXML = namedtuple('InterpretedXML', ['xml', 'types', 'inputs']) # Parsed XML, dictionary(modelElementPath) -> type, dictionary(formElementPath) -> XFormsInput
@ -27,25 +32,63 @@ def create(appName, toolName):
# TODO: read-only/immutable # TODO: read-only/immutable
typeToFreeCADTypeDict = { typeToFreeCADTypeDict = {
# TODO:do an XML namespace lookup instead of comparing a constant. # TODO:do an XML namespace lookup instead of comparing a constant.
'xsd:decimal': 'App::PropertyFloat', 'xsd:decimal': FreeCADType(type='App::PropertyFloat', defaultForType=0.0, maybeEnumValues=None),
'xsd:string': 'App::PropertyString', 'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None),
} }
def typeToFreeCADType(type, maybeEnum): def getShortPath(root, elem, root_needs_dot = True):
if maybeEnum is not None: if isinstance(root, etree._ElementTree):
return 'App::PropertyEnumeration' root = root.getroot()
elif type.startswith('mime:'): if root == elem:
return MIMETypeToFreeCADType(MIMEType[5:]) return '.'
else:
parent = elem.getparent()
parentChildren = list(parent) # convert to list of children
index = parentChildren.index(elem)
return getShortPath(root, parent) + '/*[' + str(index) + ']'
def typeToFreeCADType(type, namespacesAtTypeElement, maybeSchema):
def escape(str): return
if type.startswith('mime:'):
return MIMETypeToFreeCADType(type[5:])
elif type in typeToFreeCADTypeDict: elif type in typeToFreeCADTypeDict:
return typeToFreeCADTypeDict[type] return typeToFreeCADTypeDict[type]
elif maybeSchema is not None and ':' in type:
# TODO: should the type be looked up using the namespaces on the 'type="xxx"' side or on the 'schema' side?
nameNs, name = type.split(':', 1)
if '"' in name or '&' in name:
raise ValueError("invaid character in type name")
if nameNs not in namespacesAtTypeElement.keys() or namespacesAtTypeElement[nameNs] != maybeSchema.attrib['targetNamespace']:
raise ValueError('namespace of type reference must match the targetNamespace of the schema')
schemaTypes = maybeSchema.findall('.//*[@name="'+name+'"]', namespaces=namespacesAtTypeElement)
if len(schemaTypes) != 1:
raise ValueError('Could not find definition for XForms type.')
else: else:
raise ArgumentException('Unsupported XForms type') schemaType = schemaTypes[0];
return schemaTypeToFreeCADType(schemaType)
else:
raise ValueError('Unsupported XForms type')
def schemaTypeToFreeCADType(schemaType):
if schemaType.tag == "{http://www.w3.org/2001/XMLSchema}simpleType":
restriction = schemaType.find('./xsd:restriction', ns)
base = restriction.attrib['base']
if ':' not in base:
raise ValueError('only restrictions of xsd:string (a.k.a. enums) are supported')
baseNs, baseName = base.split(':', 1)
if baseName != 'string' or baseNs not in restriction.nsmap.keys() or restriction.nsmap[baseNs] != ns['xsd']:
raise ValueError('only restrictions of xsd:string (a.k.a. enums) are supported')
enumCases = restriction.findall('./xsd:enumeration', ns)
enumValues = [enumCase.attrib['value'] for enumCase in enumCases]
return FreeCADType(type = 'App::PropertyEnumeration', maybeEnumValues = enumValues, defaultForType = (enumValues[0] if len(enumValues) > 0 else None))
elif schemaType.tag == "{http://www.w3.org/2001/XMLSchema}complexType":
return ValueError("Complex XML chema types are not supported")
def MIMETypeToFreeCADType(MIMEType): def MIMETypeToFreeCADType(MIMEType):
if MIMEType == 'image/svg+xml': if MIMEType == 'image/svg+xml':
return 'App::PropertyLink' return FreeCADType(type='App::PropertyLink', defaultForType=None, maybeEnumValues=None)
else: else:
raise ArgumentException('Unsupported MIME type') raise ValueError('Unsupported MIME type')
def encode_bytes(bytestring): def encode_bytes(bytestring):
try: try:
@ -64,29 +107,61 @@ def decode_bytes(encoding_and_string):
else: else:
raise ValueError("invalid encoding: expected utf-8 or base64") raise ValueError("invalid encoding: expected utf-8 or base64")
def toSimpleName(name):
return re.sub(r'( |[^-a-zA-Z0-9])+', ' ', name).title().replace(' ', '')
def toUniqueSimpleName(name, mutableNextUnique):
m = re.match(r'^((.*[^0-9])?)([0-9]*)$', name)
base = m.group(1)
counter = m.group(3)
if counter == '' and mutableNextUnique[base] == 0:
mutableNextUnique[name] = 1
elif counter == '':
counter = str(mutableNextUnique[name])
mutableNextUnique[name] = mutableNextUnique[name] + 1
elif int(counter) > mutableNextUnique[name]:
mutableNextUnique[name] = str(int(counter)+1)
else:
counter = str(mutableNextUnique[name])
mutableNextUnique[name] = mutableNextUnique[name] + 1
return base + counter
class XternalAppsParametricTool(): class XternalAppsParametricTool():
def __init__(self, obj, appName, toolName): def __init__(self, obj, appName, toolName):
self.Type = "XternalAppsParametricTool" self.Type = "XternalAppsParametricTool"
self.AppName = appName self.AppName = appName
self.ToolName = toolName self.ToolName = toolName
self.MonitorChanges = False;
obj.Proxy = self obj.Proxy = self
self.ModelInstance = self.getDefaultModelInstance(obj)
self.ModelToProperties = {} self.types = self.xmlTypesToPython(self.Tool.XForms)
self.ModelOnChanged = defaultdict(set) self.defaults = self.xmlDefaultsToPython(self.Tool.XForms, self.types)
self.createPropertiesFromXML(obj) self.form = self.xmlFormToPython(self.Tool.XForms, self.types)
self.ModelInstance = self.defaults
self.createProperties(obj, self.types, self.ModelInstance, self.form)
self.MonitorChanges = True;
return
def __getstate__(self): def __getstate__(self):
copied = self.__dict__.copy() copied = self.__dict__.copy()
copied['ModelInstance'] = encode_bytes(etree.tostring(copied['ModelInstance'])) copied['ModelInstance'] = list(copied['ModelInstance'].items())
del copied['types']
del copied['defaults']
del copied['form']
return copied return copied
def __setstate__(self, state): def __setstate__(self, state):
if state: if state:
state['ModelInstance'] = etree.fromstring(decode_bytes(state['ModelInstance'])) state['ModelInstance'] = dict(state['ModelInstance'])
self.__dict__ = state self.__dict__ = state
self.types = self.xmlTypesToPython(self.Tool.XForms)
self.defaults = self.xmlDefaultsToPython(self.Tool.XForms, self.types)
self.form = self.xmlFormToPython(self.Tool.XForms, self.types)
def getDefaultModelInstance(self, obj): def getDefaultModelInstance(self, obj):
xml = etree.parse(self.Tool.XForms) xml = etree.parse(self.Tool.XForms, parser=parser)
model = xml.find('./xforms:model', ns) model = xml.find('./xforms:model', ns)
instanceDocument = etree.ElementTree(model.find('./xforms:instance/*', ns)) instanceDocument = etree.ElementTree(model.find('./xforms:instance/*', ns))
return deepcopy(instanceDocument) return deepcopy(instanceDocument)
@ -98,6 +173,27 @@ class XternalAppsParametricTool():
"""TODO""" """TODO"""
def onChanged(self, obj, prop): def onChanged(self, obj, prop):
if self.MonitorChanges:
try:
self.MonitorChanges = False
inputs = [input for input in self.form.values() if input.simpleName == prop]
if len(inputs) == 1:
input = inputs[0]
newModelValue = getattr(obj, prop)
if input.maybeEnum:
newModelValue = input.maybeEnum[newModelValue]
self.ModelInstance[input.modelElementPath] = newModelValue
for other in self.form.values():
if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
newFormValue = newModelValue
if other.maybeEnum:
newFormValue = [f for f, m in other.maybeEnum.items() if m == newModelValue][0]
setattr(obj, other.simpleName, newFormValue)
obj.setExpression(other.simpleName, None)
finally:
self.MonitorChanges = True
return
if hasattr(self, 'SimpleInputNameToInput'): if hasattr(self, 'SimpleInputNameToInput'):
inputPath = self.SimpleInputNameToInput.get(prop, None) inputPath = self.SimpleInputNameToInput.get(prop, None)
if inputPath is not None: if inputPath is not None:
@ -124,114 +220,99 @@ class XternalAppsParametricTool():
if type is None: if type is None:
raise Exception('Could not find type for ' + modelElementPath) raise Exception('Could not find type for ' + modelElementPath)
path = xml.getelementpath(xmlXFormsElement) path = xml.getelementpath(xmlXFormsElement)
path = getShortPath(xml, xmlXFormsElement) # YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
return (path, xmlXFormsElement, modelElementPath, type) return (path, xmlXFormsElement, modelElementPath, type)
def interpretXML(self): def xmlFormToPython(self, form_xml, types):
"""Parse the self.Tool.XForms document, and return """Parse the …-form.xml document, and return
* the parsed xml, a dictionary form[form_path] = ("model_path", "label", enum_labels?)"""
* a dictionary types[path] = "type"
* a dictionary inputs[path] = (xml_input_element, xml_model_element, type).""" xml = etree.parse(form_xml, parser=parser) # self.Tool.XForms
types = {} model_root = xml.find('./xforms:model', ns)
#modelInstance = {} instanceDocument = etree.ElementTree(model_root.find('./xforms:instance/*', ns))
inputs = {} inputs = {}
nextUniqueSimpleName = defaultdict(lambda: 0)
xml = etree.parse(self.Tool.XForms)
model = xml.find('./xforms:model', ns)
instanceDocument = etree.ElementTree(model.find('./xforms:instance/*', ns))
# register all xform:bind to types[pathToTargetElement]
for bind in model.findall('xforms:bind', ns):
for bound in instanceDocument.findall(bind.attrib['ref'], namespaces=bind.nsmap):
path = instanceDocument.getelementpath(bound)
# TODO: if has attrib type then …
type = bind.attrib['type']
# TODO: I guess XForms implicitly allows intersection types by using several bind statements?
types[path] = type
# TODO: "required" field
# register all inputs to inputs[pathToElement] # register all inputs to inputs[pathToElement]
for group in xml.findall('./xforms:group', ns): for group in xml.findall('./xforms:group', ns):
for input in group.findall('./xforms:input', ns): for input in group.findall('./xforms:input', ns):
path, xmlXFormsElement, modelElementPath, type = self.interpretFormElement(input, xml, instanceDocument, types) path, xmlXFormsElement, modelElementPath, _type = self.interpretFormElement(input, xml, instanceDocument, types)
inputs[path] = XFormsInput(input=xmlXFormsElement, modelElementPath=modelElementPath, type=type, maybeEnum=None, InputValueToModelValue = None, ModelValueToInputValue = None)
label = input.attrib['label']
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
groupName = "/".join(input.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None
# input=xmlXFormsElement,
inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=None, groupName=groupName) # type=type,
for select1 in group.findall('./xforms:select1', ns): for select1 in group.findall('./xforms:select1', ns):
path, xmlXFormsElement, modelElementPath, type = self.interpretFormElement(select1, xml, instanceDocument, types) path, xmlXFormsElement, modelElementPath, _type = self.interpretFormElement(select1, xml, instanceDocument, types)
label = select1.attrib['label']
simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
groupName = "/".join(select1.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None
# Gather the allowed elements for the enum # Gather the allowed elements for the enum
enum = {} enum = {}
for item in select1.findall('./xforms:item', ns): for item in select1.findall('./xforms:item', ns):
enum[item.attrib['label']] = item.attrib['value'] enum[item.attrib['label']] = item.attrib['value']
inputs[path] = XFormsInput(input=xmlXFormsElement, modelElementPath=modelElementPath, type=type, maybeEnum=enum, InputValueToModelValue = None, ModelValueToInputValue = None) # input=xmlXFormsElement,
return InterpretedXML(xml=xml, types=types, inputs=inputs) # modelInstance, inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=enum, groupName=groupName) # type=type,
def toSimpleName(self, name): return inputs
return re.sub(r'( |[^-a-zA-Z0-9])+', ' ', name).title().replace(' ', '')
def toUniqueSimpleName(self, name, mutableNextUnique): def xmlTypesToPython(self, model_xml):
m = re.match(r'^((.*[^0-9])?)([0-9]*)$', name) """Parse the …-model.xml document, and return
base = m.group(1) a dictionary model[model_path] = ("FreeCAD type", FreeCADValue)."""
counter = m.group(3)
if counter == '' and mutableNextUnique[base] == 0:
mutableNextUnique[name] = 1
elif counter == '':
counter = str(mutableNextUnique[name])
mutableNextUnique[name] = mutableNextUnique[name] + 1
elif int(counter) > mutableNextUnique[name]:
mutableNextUnique[name] = str(int(counter)+1)
else:
counter = str(mutableNextUnique[name])
mutableNextUnique[name] = mutableNextUnique[name] + 1
return base + counter
def createPropertiesFromXML(self, obj): xml = etree.parse(model_xml, parser=parser) # self.Tool.XForms
xml, types, inputs = self.interpretXML() # modelInstance, model_root = xml.find('./xforms:model', ns)
simpleInputNameToInput = {} instanceDocument = etree.ElementTree(model_root.find('./xforms:instance/*', ns))
inputToSimpleInputName = {} maybeSchema = model_root.findall('./xsd:schema', ns)
nextUniqueSimpleName = defaultdict(lambda: 0) maybeSchema = None if len(maybeSchema) != 1 else maybeSchema[0];
inputs2 = {}
for inputPath, (input, modelElementPath, type, maybeEnum, _1, _2) in inputs.items():
simpleName = self.toUniqueSimpleName(self.toSimpleName(input.attrib['label']), nextUniqueSimpleName)
simpleInputNameToInput[simpleName] = inputPath
inputToSimpleInputName[inputPath] = simpleName
group = "/".join(input.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None types = {}
for bind in model_root.findall('xforms:bind', ns):
for bound in instanceDocument.findall(bind.attrib['ref'], namespaces=bind.nsmap):
path = instanceDocument.getelementpath(bound)
# TODO: if has attrib type then …
type = bind.attrib['type']
type = typeToFreeCADType(type, bind.nsmap, maybeSchema)
# TODO: I guess XForms implicitly allows intersection types by using several bind statements?
types[path] = type
# TODO: "required" field
return types
loadedValue = self.ModelInstance.find(modelElementPath).text def xmlDefaultsToPython(self, model_xml, types):
xml = etree.parse(model_xml, parser=parser) # self.Tool.XForms
model_root = xml.find('./xforms:model', ns)
instanceDocument = etree.ElementTree(model_root.find('./xforms:instance/*', ns))
obj.addProperty(typeToFreeCADType(type, maybeEnum), defaults = {}
for modelElement in instanceDocument.findall('//*', ns):
path = instanceDocument.getelementpath(modelElement)
default = modelElement.text
if default is None:
default = types[path].defaultForType
defaults[path] = default
return defaults
def createProperties(self, obj, types, defaults, form):
for key, (modelElementPath, label, simpleName, maybeEnum, groupName) in form.items():
obj.addProperty(types[modelElementPath].type,
simpleName, simpleName,
group, groupName,
input.attrib['label'] + '\nA value of type ' + type) label + '\nA value of type ' + types[modelElementPath].type)
default = defaults[modelElementPath]
inputValueToModelValue = str
modelValueToInputValue = lambda x: x
if maybeEnum is not None: if maybeEnum is not None:
enumWithSimpleNames = { self.toSimpleName(k): v for k, v in maybeEnum.items() } setattr(obj, simpleName, list(maybeEnum.keys()))
setattr(obj, simpleName, list(enumWithSimpleNames.keys())) print(default, maybeEnum)
inputValueToModelValue = enumWithSimpleNames.get # TODO: use a bidirectional dict
modelValueToInputValue = {v: l for l, v in enumWithSimpleNames.items()}.get default = [k for k, v in maybeEnum.items() if v == default][0]
setattr(obj, simpleName, default)
if loadedValue is not None:
print("setting " + simpleName + "(full label = " + input.attrib['label'] + ") to " + str(loadedValue))
setattr(obj, simpleName, modelValueToInputValue(loadedValue))
# TODO: refactor this!
inputs2[inputPath] = XFormsInput(
input=input,
modelElementPath=modelElementPath,
type=type,
maybeEnum=maybeEnum,
InputValueToModelValue = inputValueToModelValue,
ModelValueToInputValue = modelValueToInputValue)
self.ModelOnChanged[modelElementPath].add(inputPath)
self.Types = types
self.Inputs = inputs2
self.SimpleInputNameToInput = simpleInputNameToInput
self.InputToSimpleInputName = inputToSimpleInputName
@property @property
def Tool(self): def Tool(self):
return ExternalAppsList.apps[self.AppName].Tools[self.ToolName] return XternalAppsList.apps[self.AppName].Tools[self.ToolName]
def execute(self, obj): def execute(self, obj):
"""This is called when the object is recomputed""" """This is called when the object is recomputed"""

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XternalApps:tool xmlns:my="http://github.com/jsmaniac/XternalApps/myTool" xmlns:XternalApps="http://github.com/jsmaniac/XternalApps" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <XternalApps:tool xmlns:my="http://github.com/jsmaniac/XternalApps/myTool" xmlns:XternalApps="http://github.com/jsmaniac/XternalApps/v1" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<XternalApps:name>MyTool</XternalApps:name> <XternalApps:name>MyTool</XternalApps:name>
<XternalApps:tooltip>This tool is my tool, it is very useful in a toolset.</XternalApps:tooltip> <XternalApps:tooltip>This tool is my tool, it is very useful in a toolset.</XternalApps:tooltip>
<XternalApps:icon>MyTool.svg</XternalApps:icon> <XternalApps:icon>MyTool.svg</XternalApps:icon>
@ -24,12 +24,20 @@
<xforms:bind ref="my:svgfile" type="mime:image/svg+xml" required="true()"/> <xforms:bind ref="my:svgfile" type="mime:image/svg+xml" required="true()"/>
<xforms:bind ref="my:option1" type="xsd:string" required="true()"/> <xforms:bind ref="my:option1" type="xsd:string" required="true()"/>
<xforms:bind ref="my:option2" type="xsd:decimal" required="true()"/> <xforms:bind ref="my:option2" type="xsd:decimal" required="true()"/>
<xforms:bind ref="my:option3" type="xsd:string" required="true()"/> <xforms:bind ref="my:option3" type="my:enum-option3" required="true()"/>
<!--<xforms:submission action="myTool.py" method="exec-double-dash" />--> <!--<xforms:submission action="myTool.py" method="exec-double-dash" />-->
<XternalApps:command medhod="exec" style="double-dash"> <XternalApps:command medhod="exec" style="double-dash">
<XternalApps:exception ref="my:svgfile" style="pipe" /> <XternalApps:exception ref="my:svgfile" style="pipe" />
<XternalApps:returns style="pipe" type="image/svg+xml" /> <XternalApps:returns style="pipe" type="image/svg+xml" />
</XternalApps:command> </XternalApps:command>
<xsd:schema targetNamespace="http://github.com/jsmaniac/XternalApps/myTool" xmlns:my="http://github.com/jsmaniac/XternalApps/myTool">
<xsd:simpleType name="enum-option3">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="foo" />
<xsd:enumeration value="bar" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
</xforms:model> </xforms:model>
<!-- Description of the user interface follows: --> <!-- Description of the user interface follows: -->
<xforms:group> <xforms:group>
@ -50,7 +58,7 @@
</xforms:select1> </xforms:select1>
<xforms:select1 ref="my:option3" label="Option Three (alt labels)"> <xforms:select1 ref="my:option3" label="Option Three (alt labels)">
<xforms:item label="Alt foo label" value="foo"/> <xforms:item label="Alt foo label" value="foo"/>
<xforms:item label="ALt bar label" value="bar"/> <xforms:item label="Alt bar label" value="bar"/>
</xforms:select1> </xforms:select1>
</xforms:group> </xforms:group>
</XternalApps:tool> </XternalApps:tool>