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:
parent
a663483c08
commit
90b5f9c269
|
@ -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))
|
||||||
|
|
||||||
|
|
10
Embed.py
10
Embed.py
|
@ -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
|
||||||
|
|
19
InitGui.py
19
InitGui.py
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user