diff --git a/AppCommand.py b/AppCommand.py
index a8091b0..ab14518 100644
--- a/AppCommand.py
+++ b/AppCommand.py
@@ -21,7 +21,7 @@ class AppCommand():
}
def Activated(self):
- p = Embed.ExternalAppInstance(self.appName)
+ p = Embed.XternalAppInstance(self.appName)
p.waitForWindow()
def IsActive(self):
diff --git a/Embed.py b/Embed.py
index 99a6163..c39c4ad 100644
--- a/Embed.py
+++ b/Embed.py
@@ -13,10 +13,10 @@ from MyX11Utils import *
# def closeEvent
class EmbeddedWindow(QtCore.QObject):
- def __init__(self, app, externalAppInstance, processId, windowId):
+ def __init__(self, app, xternalAppInstance, processId, windowId):
super(EmbeddedWindow, self).__init__()
self.app = app
- self.externalAppInstance = externalAppInstance
+ self.xternalAppInstance = xternalAppInstance
self.processId = processId
self.windowId = windowId
self.mdi = Gui.getMainWindow().findChild(QtGui.QMdiArea)
@@ -66,12 +66,12 @@ class EmbeddedWindow(QtCore.QObject):
self.xwd.setParent(None)
self.timer.stop()
# remove from dictionary of found windows
- self.externalAppInstance.foundWindows.pop(self.windowId, None)
+ self.xternalAppInstance.foundWindows.pop(self.windowId, None)
# avoid GC
- self.externalAppInstance.closedWindows[self.windowId] = self
+ self.xternalAppInstance.closedWindows[self.windowId] = self
# re-attach in case it didn't close (confirmation dialog etc.)
print('waitForWindow')
- self.externalAppInstance.waitForWindow()
+ self.xternalAppInstance.waitForWindow()
# try:
# self.xw = QtGui.QWindow.fromWinId(self.windowId)
# self.xwd = QtGui.QWidget.createWindowContainer(self.xw)
@@ -119,9 +119,9 @@ def deleted(widget):
except:
return True
-class ExternalAppInstance(QtCore.QObject):
+class XternalAppInstance(QtCore.QObject):
def __init__(self, appName):
- super(ExternalAppInstance, self).__init__()
+ super(XternalAppInstance, self).__init__()
self.app = XternalAppsList.apps[appName]
# Start the application
# TODO: popen_process shouldn't be exposed to in-document scripts, it would allow them to redirect output etc.
diff --git a/InitGui.py b/InitGui.py
index 9752545..92d760b 100644
--- a/InitGui.py
+++ b/InitGui.py
@@ -72,8 +72,8 @@ class XternalAppsWorkbench(Workbench):
for toolName in XternalAppsList.apps[self.appName].Tools]
# Create menus and toolbars
- self.appendMenu("ExternalApplications", self.list)
- self.appendToolbar("ExternalApplications", self.list)
+ self.appendMenu("XternalApplications", self.list)
+ self.appendToolbar("XternalApplications", self.list)
def Activated(self):
pass
diff --git a/ReloadCommand.py b/ReloadCommand.py
index 925ab72..24e19f9 100644
--- a/ReloadCommand.py
+++ b/ReloadCommand.py
@@ -26,7 +26,7 @@ class ReloadCommand():
def GetResources(self):
return {
'Pixmap': os.path.dirname(__file__) + '/icons/' + "reload.svg",
- 'Accel': "Shit+R", # R for Reload
+ 'Accel': "Ctrl+R", # R for Reload
'MenuText': "Reload XternalApps (developper tool)",
'ToolTip': "Reload some modules of the XternalApps workbenches, needed for development only.",
}
diff --git a/SearchTools/SearchTools.py b/SearchTools/SearchTools.py
new file mode 100644
index 0000000..5bced34
--- /dev/null
+++ b/SearchTools/SearchTools.py
@@ -0,0 +1,106 @@
+if True:
+ from PySide import QtGui
+ mw = Gui.getMainWindow()
+ mdi = mw.findChild(QtGui.QMdiArea)
+
+ mw.findChildren(QtGui.QToolBar, 'XternalApplications')
+ mw.findChildren(QtGui.QToolBar, 'XternalApplications')[0]
+
+ wdg = QtGui.QWidget()
+ lay = QtGui.QGridLayout(wdg)
+ mwx = QtGui.QMainWindow()
+
+ sea = QtGui.QLineEdit()
+ lay.addWidget(sea)
+
+ lsv = QtGui.QListView()
+ sim = QtGui.QStandardItemModel()
+ flt = QtCore.QSortFilterProxyModel()
+ flt.setSourceModel(sim)
+ flt.setFilterCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseInsensitive)
+ sea.textChanged.connect(flt.setFilterWildcard)
+ # make the QListView non-editable
+ lsv.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
+ lsv.setModel(flt)
+ lay.addWidget(lsv)
+
+ mwx.setCentralWidget(wdg)
+ mdi.addSubWindow(mwx)
+
+ all_tbs = set()
+ for wbname, workbench in Gui.listWorkbenches().items():
+ try:
+ tbs = workbench.listToolbars()
+ except:
+ continue
+ # careful, tbs contains all the toolbars of the workbench, including shared toolbars
+ for tb in tbs:
+ all_tbs.add(tb)
+
+ for toolbar_name in all_tbs:
+ for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbar_name):
+ #header = QtGui.QPushButton(toolbar_name)
+ #lay.addWidget(header)
+
+ #sim.insertRow(sim.rowCount())
+ #sim.setData(sim.index(sim.rowCount() - 1, 0), toolbar_name)
+ sim.appendRow(QtGui.QStandardItem(toolbar_name))
+
+ for bt in the_toolbar.findChildren(QtGui.QToolButton):
+ text = bt.text()
+ if text != '':
+ print(text)
+ # TODO: there also is the tooltip
+ icon = bt.icon()
+
+ # To preview the icon, assign it as the icon of a dummy button.
+ #but3 = QtGui.QPushButton(text)
+ #but3.setIcon(icon)
+ #lay.addWidget(but3)
+
+ #slm.insertRow(slm.rowCount())
+ #slm.setData(slm.index(slm.rowCount() - 1, 0), icon)
+ #slm.setData(slm.index(slm.rowCount() - 1, 1), text)
+ sim.appendRow(QtGui.QStandardItem(icon, text))
+
+ #mwx = QtGui.QMainWindow()
+ #mwx.show()
+ #mdi.addSubWindow(mwx)
+ #mdi.setWindowIcon(icon) # probably sets the default icon to use for windows without an icon?
+ mwx.setWindowIcon(icon) # untested
+ mwx.show()
+
+ # for wbname, workbench in Gui.listWorkbenches().items():
+ # try:
+ # tbs = workbench.listToolbars()
+ # # careful, tbs contains all the toolbars of the workbench, including shared toolbars
+ # for tb in mw.findChildren(QtGui.QToolBar, 'XternalApplications'):
+ # for bt in tb.findChildren(QtGui.QToolButton):
+ # text = bt.text()
+ # if text != '':
+ # # TODO: there also is the tooltip
+ # icon = bt.icon()
+
+ # # To preview the icon, assign it as the icon of a dummy window.
+ # mdi.setWindowIcon(icon) # probably sets the default icon to use for windows without an icon?
+ # mwx.setWindowIcon(icon) # untested
+ # except:
+ # pass
+
+
+
+from PySide import QtGui
+qwd = QtGui.QWidget()
+but1 = QtGui.QPushButton("hi")
+but2 = QtGui.QPushButton("hello")
+lay = QtGui.QGridLayout(qwd)
+lay.addWidget(but1)
+lay.addWidget(but2)
+mwx = QtGui.QMainWindow()
+mwx.setCentralWidget(qwd)
+mw = Gui.getMainWindow()
+mdi = mw.findChild(QtGui.QMdiArea)
+mdi.addSubWindow(mwx)
+mwx.show()
+but3 = QtGui.QPushButton("XXX")
+lay.addWidget(but3)
diff --git a/XternalAppsList.py b/XternalAppsList.py
index 9350917..ba744db 100644
--- a/XternalAppsList.py
+++ b/XternalAppsList.py
@@ -30,7 +30,7 @@ class Tool():
return Tool(appName=appName,
toolName = getSingletonFromXML(xml, './XternalApps:name').text,
xForms = xForms,
- toolTip = getSingletonFromXML(xml, './XternalApps:tooltip').text,
+ toolTip = getSingletonFromXML(xml, './XternalApps:tooltip').text or '',
icon = os.path.dirname(__file__) + '/icons/' + appName + '/' + getSingletonFromXML(xml, './XternalApps:icon').text,
extendedDescription = getSingletonFromXML(xml, './XternalApps:extended-description').text,
openHelpFile = None)
diff --git a/XternalAppsParametricTool.py b/XternalAppsParametricTool.py
index df02ad1..ce3d9d7 100644
--- a/XternalAppsParametricTool.py
+++ b/XternalAppsParametricTool.py
@@ -12,12 +12,24 @@ import Part
parser = etree.XMLParser(resolve_entities=False)
-FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues', 'maybeMIMEType'])
+FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues', 'maybeMIMEType', 'fromString'])
XFormsInput = namedtuple('XFormsInput', ['modelElementPath', 'label', 'simpleName', 'maybeEnum', 'groupName', 'relevance']) #'type', 'input', 'InputValueToModelValue', 'ModelValueToInputValue'
XFormsEnum = namedtuple('XFormsEnum', ['labels', 'values'])
InterpretedXML = namedtuple('InterpretedXML', ['xml', 'types', 'inputs']) # Parsed XML, dictionary(modelElementPath) -> type, dictionary(formElementPath) -> XFormsInput
+# Safe printing of unknown strings
+# This does not aim to have an exact representation of the string, just enough to display in error messages
+def safeErr(s):
+ s = str(s)
+ result = ''
+ for c in s:
+ if c in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :_-':
+ result += c
+ else:
+ result += ('\\u%04x' % + ord(c))
+ return result
+
def CreateCommand(appName, toolName):
App.ActiveDocument.openTransaction('Create parametric %s from %s'%(toolName, appName))
FreeCADGui.addModule("XternalAppsParametricTool")
@@ -27,15 +39,17 @@ def CreateCommand(appName, toolName):
def create(appName, toolName):
sel = FreeCADGui.Selection.getSelection()
name = appName + toolName
- obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", name)
+ #obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", name)
+ obj = App.ActiveDocument.addObject("Part::FeaturePython", name)
XternalAppsParametricTool(obj, appName, toolName, sel)
return obj
# TODO: read-only/immutable
typeToFreeCADTypeDict = {
# TODO:do an XML namespace lookup instead of comparing a constant.
- 'xsd:decimal': FreeCADType(type='App::PropertyFloat', defaultForType=0.0, maybeEnumValues=None, maybeMIMEType=None),
- 'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None, maybeMIMEType=None),
+ 'xsd:decimal': FreeCADType(type='App::PropertyFloat', defaultForType=0.0, maybeEnumValues=None, maybeMIMEType=None, fromString=float),
+ 'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None, maybeMIMEType=None, fromString=lambda x: x),
+ 'xsd:integer': FreeCADType(type='App::PropertyInteger', defaultForType=0, maybeEnumValues=None, maybeMIMEType=None, fromString=int),
}
def getShortPath(root, elem, root_needs_dot = True):
@@ -69,7 +83,7 @@ def typeToFreeCADType(type, namespacesAtTypeElement, maybeSchema):
schemaType = schemaTypes[0];
return schemaTypeToFreeCADType(schemaType)
else:
- raise ValueError('Unsupported XForms type')
+ raise ValueError('Unsupported XForms type ' + safeErr(type))
def schemaTypeToFreeCADType(schemaType):
if schemaType.tag == "{http://www.w3.org/2001/XMLSchema}simpleType":
@@ -82,13 +96,13 @@ def schemaTypeToFreeCADType(schemaType):
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', defaultForType = (enumValues[0] if len(enumValues) > 0 else None), maybeEnumValues = enumValues, maybeMIMEType=None)
+ return FreeCADType(type = 'App::PropertyEnumeration', defaultForType = (enumValues[0] if len(enumValues) > 0 else None), maybeEnumValues = enumValues, maybeMIMEType=None, fromString=lambda x: x)
elif schemaType.tag == "{http://www.w3.org/2001/XMLSchema}complexType":
return ValueError("Complex XML chema types are not supported")
def MIMETypeToFreeCADType(MIMEType):
if MIMEType == 'image/svg+xml':
- return FreeCADType(type='App::PropertyLink', defaultForType=None, maybeEnumValues=None, maybeMIMEType = MIMEType)
+ return FreeCADType(type='App::PropertyLink', defaultForType=None, maybeEnumValues=None, maybeMIMEType = MIMEType, fromString=lambda x: x)
else:
raise ValueError('Unsupported MIME type')
@@ -129,28 +143,134 @@ def exportSVG(obj, svgfile):
# TODO: modify the SVG to set a fake Inkscape version, to avoid the pop-up dialog.
+class XternalAppsParametricToolViewProvider():
+ def __init__(self, vobj):
+ """
+ Set this object to the proxy object of the actual view provider
+ """
+
+ vobj.Proxy = self
+
+ def attach(self, vobj):
+ """
+ Setup the scene sub-graph of the view provider, this method is mandatory
+ """
+ self.ViewObject = vobj
+ self.Object = vobj.Object
+
+ def updateData(self, fp, prop):
+ """
+ If a property of the handled feature has changed we have the chance to handle this here
+ """
+ print('VVVVVVVVVVVVVVVVVV', repr(fp), repr(prop))
+ return
+
+ def getDisplayModes(self,vobj):
+ """
+ Return a list of display modes.
+ """
+ return []
+
+ def getDefaultDisplayMode(self):
+ """
+ Return the name of the default display mode. It must be defined in getDisplayModes.
+ """
+ return "Shaded"
+
+ def setDisplayMode(self,mode):
+ """
+ Map the display mode defined in attach with those defined in getDisplayModes.
+ Since they have the same names nothing needs to be done.
+ This method is optional.
+ """
+ return mode
+
+ def onChanged(self, vp, prop):
+ """
+ Print the name of the property that has changed
+ """
+ App.Console.PrintMessage("Change property: " + str(prop) + "\n")
+
+ def getIcon(self):
+ """
+ Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown.
+ """
+
+ return """
+ /* XPM */
+ static const char * ViewProviderBox_xpm[] = {
+ "16 16 6 1",
+ " c None",
+ ". c #141010",
+ "+ c #615BD2",
+ "@ c #C39D55",
+ "# c #000000",
+ "$ c #57C355",
+ " ........",
+ " ......++..+..",
+ " .@@@@.++..++.",
+ " .@@@@.++..++.",
+ " .@@ .++++++.",
+ " ..@@ .++..++.",
+ "###@@@@ .++..++.",
+ "##$.@@$#.++++++.",
+ "#$#$.$$$........",
+ "#$$####### ",
+ "#$$#$$$$$# ",
+ "#$$#$$$$$# ",
+ "#$$#$$$$$# ",
+ " #$#$$$$$# ",
+ " ##$$$$$# ",
+ " ####### "};
+ """
+
+ def claimChildren(self):
+ return self.Object.Proxy.getChildren(self.Object)
+
+ def __getstate__(self):
+ """
+ Called during document saving.
+ """
+ return None
+
+ def __setstate__(self,state):
+ """
+ Called during document restore.
+ """
+ print('AAAAAAAAAAAAAAAAAAAYYYYYYYYYYYYYYYYYYYYYYYYY', repr(self), repr(state))
+ return None
+
+ def onDocumentRestored(self, obj):
+ print('AAAAAAAAAAAAAAAAAAAXXXXXXXXXXXXXXXXXXXXXXXXX', repr(self), repr(obj))
class XternalAppsParametricTool():
- def __init__(self, obj, appName, toolName, sel=[]):
+ def init1(self, appName, toolName):
self.Type = "XternalAppsParametricTool"
self.AppName = appName
self.ToolName = toolName
- self.MonitorChanges = False;
+ self.MonitorChanges = False
+
+ def init2(self, obj):
+ self.Object = obj
obj.Proxy = self
-
+
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)
-
+
+ # TODO: on restore, reload the model instance from obj (or recompute it on the fly)
self.ModelInstance = self.defaults
- self.createProperties(obj, self.types, self.ModelInstance, self.form)
self.oldExpressionEngine = obj.ExpressionEngine
+ def init3(self, obj):
self.MonitorChanges = True
- #pprint.pprint(self.types)
- #pprint.pprint(self.form)
+ def __init__(self, obj, appName, toolName, sel=[]):
+ self.init1(appName, toolName)
+ self.init2(obj)
+ self.createProperties(obj, self.types, self.ModelInstance, self.form)
+ self.init3(obj)
# Special treatment for the "primary" form field
primary = [input for input in self.form.values() if input.relevance == 'primary']
@@ -160,30 +280,62 @@ class XternalAppsParametricTool():
# Display the contents of the primary form element as children in the tree view
if type in ['App::PropertyLink', 'App::PropertyLinkList']:
- self.form['FreeCADGroup'] = XFormsInput(modelElementPath=primary.modelElementPath, label='Group', simpleName='Group', maybeEnum=primary.maybeEnum, groupName='Base', relevance='primary')
+ #self.form['FreeCADGroup'] = XFormsInput(modelElementPath=primary.modelElementPath, label='Group', simpleName='Group', maybeEnum=primary.maybeEnum, groupName='Base', relevance='primary')
+ pass
if type == 'App::PropertyLink' and len(sel) >= 1:
setattr(obj, primary.simpleName, sel[0])
elif type == 'App::PropertyLinkList':
setattr(obj, primary.simpleName, sel)
- return
+ XternalAppsParametricToolViewProvider(obj.ViewObject)
+ self.execute(obj)
+
+ def getChildren(self, obj):
+ primary = [input for input in self.form.values() if input.relevance == 'primary']
+ if len(primary) == 1:
+ primary = primary[0]
+ type = self.types[primary.modelElementPath].type
+ if type == 'App::PropertyLink':
+ return [getattr(obj, primary.simpleName)]
+ elif type == 'App::PropertyLinkList':
+ return getattr(obj, primary.simpleName)
+ return []
def __getstate__(self):
+ return { "AppName": self.AppName, "ToolName": self.ToolName }
+
+ print('XXX_GET_STATE')
+ print('self', repr(self))
copied = self.__dict__.copy()
copied['ModelInstance'] = list(copied['ModelInstance'].items())
del copied['types']
del copied['defaults']
del copied['form']
+ print('copied', repr(copied))
return copied
def __setstate__(self, state):
+ print('XXX_SET_STATE')
+ print(repr(self), repr(state))
if state:
- state['ModelInstance'] = dict(state['ModelInstance'])
- 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)
+ # TODO: always sanitize data that is restored, for security reasons.
+ #self.AppName = str(state['AppName'])
+ #self.ToolName = str(state['ToolName'])
+ #self.__init__(obj, self.AppName, self.ToolName, sel=[])
+ self.init1(str(state['AppName']), str(state['ToolName']))
+ #state['ModelInstance'] = dict(state['ModelInstance'])
+ #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 onDocumentRestored(self, obj):
+ print('XXX_ON_DOCUMENT_RESTORED')
+ self.init2(obj)
+ self.reloadProperties(obj, self.form)
+ self.init3(obj)
+ #self.__init__(obj, self.AppName, self.ToolName, sel=[])
def onChanged(self, obj, prop):
import sys
@@ -215,31 +367,37 @@ class XternalAppsParametricTool():
obj.setExpression(other.simpleName, None)
self.oldExpressionEngine = set([k for k, v in obj.ExpressionEngine])
- _, input = lookup(self.form, lambda input: input.simpleName, prop)
- if input:
- newModelValue = getattr(obj, prop)
- if input.maybeEnum:
- newModelValue = input.maybeEnum[newModelValue]
- # The Group property always contains a list, but we may use it to represent a link to a single element.
- if prop == 'Group' and self.types[input.modelElementPath].type == 'App::PropertyLink':
- if len(newModelValue) > 0:
- newModelValue = newModelValue[0]
- else:
- newModelValue = None
- self.ModelInstance[input.modelElementPath] = newModelValue
- for other in self.form.values():
- if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
- newFormValue = newModelValue
- #print('newModelValue', newModelValue)
- if other.maybeEnum:
- newFormValue = [f for f, m in other.maybeEnum.items() if m == newModelValue][0]
- #print(prop, newFormValue, other.simpleName, dict(obj.ExpressionEngine).get(prop), dict(obj.ExpressionEngine).get(other.simpleName))
- #obj.setExpression(other.simpleName, dict(obj.ExpressionEngine).get(prop))
- #print(obj, other.simpleName, newFormValue)
- setattr(obj, other.simpleName, newFormValue)
+ #######################################
+ self.setModelFromInput(obj, prop)
+ #######################################
finally:
#print('MonitorChanges = ' + str(restoreMonitorChanges))
self.MonitorChanges = restoreMonitorChanges
+
+ def setModelFromInput(self, obj, prop):
+ _, input = lookup(self.form, lambda input: input.simpleName, prop)
+ if input:
+ newModelValue = getattr(obj, prop)
+ if input.maybeEnum:
+ newModelValue = input.maybeEnum[newModelValue]
+ # The Group property always contains a list, but we may use it to represent a link to a single element.
+ if prop == 'Group' and self.types[input.modelElementPath].type == 'App::PropertyLink':
+ if len(newModelValue) > 0:
+ newModelValue = newModelValue[0]
+ else:
+ newModelValue = None
+ print(self.ModelInstance, input.modelElementPath, newModelValue)
+ self.ModelInstance[input.modelElementPath] = newModelValue
+ for other in self.form.values():
+ if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
+ newFormValue = newModelValue
+ #print('newModelValue', newModelValue)
+ if other.maybeEnum:
+ newFormValue = [f for f, m in other.maybeEnum.items() if m == newModelValue][0]
+ #print(prop, newFormValue, other.simpleName, dict(obj.ExpressionEngine).get(prop), dict(obj.ExpressionEngine).get(other.simpleName))
+ #obj.setExpression(other.simpleName, dict(obj.ExpressionEngine).get(prop))
+ #print(obj, other.simpleName, newFormValue)
+ setattr(obj, other.simpleName, newFormValue)
def interpretFormElement(self, xmlXFormsElement, xml, instanceDocument, types):
# TODO: is it safe to pass input unprotected here?
@@ -348,7 +506,7 @@ class XternalAppsParametricTool():
default = modelElement.text
if default is None:
default = types[path].defaultForType
- defaults[path] = default
+ defaults[path] = types[path].fromString(default)
return defaults
def createProperties(self, obj, types, defaults, form):
@@ -362,155 +520,264 @@ class XternalAppsParametricTool():
setattr(obj, simpleName, list(maybeEnum.keys()))
# TODO: use a bidirectional dict
default = [k for k, v in maybeEnum.items() if v == default][0]
- setattr(obj, simpleName, default)
+ try:
+ setattr(obj, simpleName, default)
+ except:
+ raise ValueError('Could not set ' + safeErr(obj) + "." + safeErr(simpleName) + " = " + safeErr(repr(default)))
+
+ def reloadProperties(self, obj, form):
+ for key, (modelElementPath, label, simpleName, maybeEnum, groupName, relevance) in form.items():
+ self.setModelFromInput(obj, simpleName)
@property
def Tool(self):
return XternalAppsList.apps[self.AppName].Tools[self.ToolName]
- def xmlCommandToPython(self, document):
+ def xmlCommandToPython(self, obj, document):
"""Parse the .xml document, and return
a pair of dictionaries accepts[model_path] = style and returns[model_path] = style."""
+ print('A')
+
xml = etree.parse(self.Tool.XForms, parser=parser)
model_root = xml.find('./xforms:model', ns)
instanceDocument = etree.ElementTree(model_root.find('./xforms:instance/*', ns))
+ print('B')
command = xml.find('./XternalApps:command', ns)
method = command.attrib['method']
commandName = command.attrib['name']
- accepts = command.find('./XternalApps:accepts', ns)
- returns = command.find('./XternalApps:returns', ns)
-
- maybeDefault = accepts.findall('./XternalApps:default', ns)
- if len(maybeDefault) == 1:
- default_style = maybeDefault[0].attrib['style']
- elif len(maybeDefault) > 1:
- raise ValueError('The accepts tag should contain at most one default tag')
- else:
- default_style = None
-
- styles = {}
+ print('C')
+ # Step 1: get the list of all fields
optionNames = {}
- # Put all the model fields in accepts[path]
for modelElement in instanceDocument.findall('//*', ns):
- path = instanceDocument.getelementpath(modelElement)
- styles[path] = default_style
- optionNames[path] = etree.QName(modelElement).localname
-
- style_is_default = {k: True for k in styles.keys()}
- for exception in accepts.findall('./XternalApps:exception', ns):
- ref = exception.attrib['ref']
- style = exception.attrib['style']
- for modelElement in instanceDocument.findall(ref, exception.nsmap):
- path = instanceDocument.getelementpath(modelElement)
- if style_is_default[path]:
- style_is_default[path] = False
- styles[path] = style
- else:
- ValueError('overlapping exceptions in command/accepts/exception')
-
- pipe_in = None
- positionalArguments = {}
- namedArguments = []
+ # Put all the model fields in optionNames[path] = optionName
+ modelElementPath = instanceDocument.getelementpath(modelElement)
+ optionNames[modelElementPath] = etree.QName(modelElement).localname
+
+ print('D')
tempfiles = []
tempdirs = []
- for modelElementPath, value in self.ModelInstance.items():
- style = styles[modelElementPath]
- type = self.types[modelElementPath]
+ try:
+ commandLine = [commandName]
+ default = None
+ pipeIn = None
+ # Step 2: generate most of the command-line, leaving a placeholder for the fields that use the default behaviour
+ print('E')
+ def formatTemplateElement(isInput, style, key, type, value, tempdirs, tempfiles):
+ print('K3X')
+ # Convert to the type expected by the tool
+ if type.type == 'App::PropertyLink' and type.maybeMIMEType == 'image/svg+xml':
+ print('K31')
+ import tempfile, os
+ d = tempfile.mkdtemp()
+ tempdirs += [d]
+ svgfile = os.path.join(d, "sketch.svg")
+ tempfiles += [svgfile]
+ print("exportSVG", repr(value), repr(svgfile))
+ exportSVG(value, svgfile)
+ value = svgfile
+ else:
+ print('K32')
+ # TODO ################# convert the value from FreeCAD to what the program supports ###################
+ value = str(value)
+
+ if style == 'value':
+ if isInput:
+ return [value]
+ else:
+ pass # TODO: e.g. ['temporary_output_file']
+ elif style == 'double-dash':
+ if isInput:
+ return ['--' + key, value]
+ else:
+ pass # TODO: e.g. ['-o', 'temporary_output_file']
+ elif style == 'pipe':
+ if isInput:
+ if pipeIn is not None:
+ raise ValueError('Only one parameter can be passed as a pipe')
+ pipeIn = value
+ else:
+ pass # TODO: output
+ return []
+ elif style == 'exitcode':
+ if isInput:
+ raise ValueError('the exitcode style can only be used for the output direction')
+ else:
+ pass # TODO: output
+ else:
+ raise ValueError('Unsupported argument-passing or value-returning style')
+ print('F')
+ for templateElement in command.findall('./*', ns):
+ print('G')
+ direction = templateElement.attrib['direction']
+ style = templateElement.attrib['style']
+ tag = templateElement.tag
+
+ print('H')
+ tagPrefix = '{'+ns['XternalApps']+'}'
+ if not tag.startswith(tagPrefix):
+ continue
+ tag = tag[len(tagPrefix):]
+
+ print('I')
+ if direction == 'input':
+ isInput = True
+ elif direction == 'output':
+ isInput = False
+ else:
+ raise ValueError('Invalid value for direction attribute')
+
+ print('J')
+ if tag == 'constant':
+ print('K1')
+ if isInput:
+ key = templateElement.attrib.get('key', None)
+ type = typeToFreeCADTypeDict['xsd:string']
+ value = templateElement.attrib['value']
+ commandLine += formatTemplateElement(isInput, style, key, type, value, tempdirs, tempfiles)
+ else:
+ raise ValueError('constant elements of a command-line input can only be part of the input, not of the output')
+ elif tag == 'default':
+ print('K2')
+ if isInput:
+ if default is not None:
+ raise ValueError('Only one default tag can be specified for a given direction')
+ default = {'style':style, 'position':len(commandLine)}
+ else:
+ pass # TODO: output
+ elif tag == 'exception':
+ print('K3')
+ ref = templateElement.attrib['ref']
+ found = False
+ if isInput:
+ for modelElement in instanceDocument.findall(ref, templateElement.nsmap):
+ found = True
+ modelElementPath = instanceDocument.getelementpath(modelElement)
+ key = optionNames[modelElementPath]
+ value = self.ModelInstance[modelElementPath]
+ type = self.types[modelElementPath]
+ commandLine += formatTemplateElement(isInput, style, key, type, value, tempdirs, tempfiles)
+ if modelElementPath in optionNames:
+ del(optionNames[modelElementPath])
+ else:
+ raise ValueError('In command-line template, the same field is referenced by two tags (e.g. exception and ignore)')
+ else:
+ found = True
+ # TODO: output
+ pass
+ if not found:
+ raise ValueError('Could not resolve reference in command-line template: ' + safeErr(ref))
+ elif tag == 'ignore':
+ print('K4')
+ ref = templateElement.attrib['ref']
+ found = False
+ if isInput:
+ for modelElement in instanceDocument.findall(ref, templateElement.nsmap):
+ found = True
+ modelElementPath = instanceDocument.getelementpath(modelElement)
+ if modelElementPath in optionNames:
+ del(optionNames[modelElementPath])
+ else:
+ raise ValueError('In command-line template, the same field is referenced by two tags (e.g. exception and ignore)')
+ else:
+ found = True
+ # TODO: output
+ pass
+ if not found:
+ raise ValueError('Could not resolve reference in command-line template')
+ else:
+ print('K5')
+ raise ValueError('Unexpected tag in command-line template:' + safeErr(tag))
- # Convert to the type expected by the tool
- if type.type == 'App::PropertyLink' and type.maybeMIMEType == 'image/svg+xml':
- import tempfile, os
- d = tempfile.mkdtemp()
- svgfile = os.path.join(d, "sketch.svg")
- tempfiles += [svgfile]
- tempdirs += [d]
- exportSVG(value, svgfile)
- value = svgfile
- else:
- # TODO ################# convert the value from FreeCAD to what the program supports ###################
- value = str(value)
+ # Step 3: replace the placeholder with the remaining input fields
+ print('L')
+ commandLineDefault = []
+ for modelElementPath, key in optionNames.items():
+ value = self.ModelInstance[modelElementPath]
+ type = self.types[modelElementPath]
+ if default is None:
+ raise ValueError('Some fields are not included in the command-line template, and no default is present. To ignore a field, use the ignore tag.')
+ style = default['style']
+ commandLineDefault += formatTemplateElement(True, style, key, type, value, tempdirs, tempfiles)
+ if default is not None:
+ position = default['position']
+ commandLine[position:position] = commandLineDefault
+
+ # Step 4: call the command
+ #for modelElementPath, value in self.ModelInstance.items():
+ # style = styles[modelElementPath]
+ # type = self.types[modelElementPath]
+
+ pipeInHandle = None
+ if pipeIn is not None:
+ pipeInHandle = open(pipeIn)
- if style == 'double-dash':
- namedArguments += ['--' + optionNames[modelElementPath], value]
- elif style == 'pipe':
- if pipe_in != None:
- raise ValueError('more then one option uses a "pipe" style')
- pipe_in = value
- elif style == 'positional':
- pos = unknown_todo() ######################################### TODO ############################
- positionalArguments[pos] = value
- elif style == 'exitcode':
- raise ValueError('exitcode is supported only for the output of the command, not for its input')
- else:
- raise ValueError('unsupported argument-passing style')
-
- positionalArguments = [v for i, vs in sorted(positionalArguments.items()) for v in vs]
- if pipe_in is not None:
- pipe_in_handle = open(pipe_in)
+ # TODO: use the XML for this
+ import tempfile, os
+ d = tempfile.mkdtemp()
+ resultFilename = os.path.join(d, "result.svg")
+ pipeOut = resultFilename
+ tempfiles += [pipeOut]
+ tempdirs += [d]
+ pipeOutHandle = open(pipeOut, 'w')
- # TODO: use the XML for this
- d = tempfile.mkdtemp()
- result_filename = os.path.join(d, "result.svg")
- pipe_out = result_filename
- tempfiles += [pipe_out]
- tempdirs += [d]
- pipe_out_handle = open(pipe_out, 'w')
+ import subprocess
+ #print(commandLine + ['stdin=' + str(pipeIn), 'stdout=' + str(pipeOut)])
+ proc = subprocess.Popen(commandLine, stdin=pipeInHandle, stdout=pipeOutHandle)
+ proc.communicate()
+ exitcode = proc.returncode
- import subprocess
- print([commandName] + positionalArguments + namedArguments + ['stdin=' + pipe_in, 'stdout=' + pipe_out])
- proc = subprocess.Popen([commandName] + positionalArguments + namedArguments, stdin=pipe_in_handle, stdout=pipe_out_handle)
- proc.communicate()
- exitcode = proc.returncode
+ if pipeInHandle is not None:
+ pipeInHandle.close()
+ with open(resultFilename, 'rb') as resultFile:
+ result = resultFile.read()
+ pipeOutHandle.close()
- pipe_in_handle.close()
- with open(result_filename, 'rb') as result_file:
- result = result_file.read()
- pipe_out_handle.close()
-
- # Circumvent bug which leaves App.ActiveDocument to an incorrect value after the newDocument + closeDocument
- oldActiveDocumentName = App.ActiveDocument.Name
- tempDocument = App.newDocument('load_svg', hidden=True)
- import importSVG
- importSVG.insert(pipe_out,'load_svg')
- solids = []
- for o in tempDocument.Objects:
- shape = o.Shape
- wire = Part.Wire(shape.Edges)
- #face = Part.Face(wire)
- solids += [wire] #face
- p = Part.makeCompound(solids)
- for obj in tempDocument.Objects:
- print("remove:" + obj.Name)
- tempDocument.removeObject(obj.Name)
- Part.show(p)
- for o in tempDocument.Objects:
- o2 = document.copyObject(o, False, False)
- App.closeDocument('load_svg')
- App.setActiveDocument(oldActiveDocumentName)
-
- for tempfile in tempfiles:
- os.remove(tempfile)
- for tempdir in tempdirs:
- os.rmdir(tempdir)
+ # Circumvent bug which leaves App.ActiveDocument to an incorrect value after the newDocument + closeDocument
+ oldActiveDocumentName = App.ActiveDocument.Name
+ tempDocument = App.newDocument('load_svg', hidden=True)
+ import importSVG
+ importSVG.insert(pipeOut,'load_svg')
+ solids = []
+ for o in tempDocument.Objects:
+ shape = o.Shape
+ wire = Part.Wire(shape.Edges)
+ #face = Part.Face(wire)
+ solids += [wire] #face
+ p = Part.makeCompound(solids)
+ for o in tempDocument.Objects:
+ print("remove:" + o.Name)
+ tempDocument.removeObject(o.Name)
+ Part.show(p)
+ print("===============================================================")
+ for o in tempDocument.Objects:
+ #o2 = document.copyObject(o, False, False)
+ #print(o2.Name)
+ obj.Shape = o.Shape
+ obj.ViewObject.DisplayMode = o.ViewObject.DisplayMode
+ break
+ #document.removeObject(o2.Name)
+ print("===============================================================!!")
+ App.closeDocument('load_svg')
+ App.setActiveDocument(oldActiveDocumentName)
+ finally:
+ pass
+ #for tempfile in tempfiles:
+ # try:
+ # os.remove(tempfile)
+ # except:
+ # pass
+ #for tempdir in tempdirs:
+ # try:
+ # os.rmdir(tempdir)
+ # except:
+ # pass
print(exitcode, result)
def execute(self, obj):
print("""This is called when the object is recomputed""")
- self.xmlCommandToPython(obj.Document)
-
- #
- #
- #
- #
-
- #
- #
- #
- #
-
-
+ self.xmlCommandToPython(obj, obj.Document)
diff --git a/exampleTool.xforms b/exampleTool.xforms
new file mode 100644
index 0000000..c99ae2d
--- /dev/null
+++ b/exampleTool.xforms
@@ -0,0 +1,70 @@
+
+
+ Fractalize
+
+ MyTool.svg
+
+
+
+
+
+
+
+ default value
+
+ bar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Page 1
+
+
+
+ Input image
+
+
+
+
+ Page 2
+
+
+
+
+
+
+
+
+
+
+
diff --git a/myTool.xforms b/myTool.xforms
index 85269a2..9efc7f5 100644
--- a/myTool.xforms
+++ b/myTool.xforms
@@ -1,20 +1,17 @@
- MyTool
- This tool is my tool, it is very useful in a toolset.
+ Fractalize
+
MyTool.svg
- Lots of text,
- blah blha bhal
- default value
-
- bar
+ 6
+ 4.0
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
- Page 1
-
-
+ Fractalize
+
+
- Input image
+ Input path
-
- Page 2
-
-
-
-
-
-
-
-
-
-
diff --git a/run-inkscape-plugin.py b/run-inkscape-plugin.py
new file mode 100755
index 0000000..eacfb7a
--- /dev/null
+++ b/run-inkscape-plugin.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+# Does not work with inkscape 1.1
+#extension_directory="$(inkscape --extension-directory)"
+
+appImage = '/home/suzanne/perso/dl/software/dessin/Inkscape-16c8184-x86_64.AppImage'
+
+import sys, os, subprocess, signal, tempfile, re
+mountProcess = subprocess.Popen([appImage, '--appimage-mount'], stdout=subprocess.PIPE)
+
+# os.path.join, but forbit .. and other tricks that would get out of the base directory.
+def joinNoDotDot(base, rel):
+ rel = os.fsencode(rel)
+ absBase = os.path.abspath(base)
+ relRequested = os.path.relpath(os.path.join(absBase, rel), start=absBase)
+ absRequested = os.path.abspath(os.path.join(absBase, relRequested))
+ commonPrefix = os.path.commonprefix([absRequested, absBase])
+ if relRequested.startswith(os.fsencode(os.pardir)):
+ raise ValueError("security check failed: requested inkscape extension is outside of inkscape extension directory 1" + repr(relRequested))
+ elif commonPrefix != absBase:
+ raise ValueError("security check failed: requested inkscape extension is outside of inkscape extension directory 2")
+ else:
+ return absRequested
+
+tempfiles=[]
+tempdirs=[]
+try:
+ d = tempfile.mkdtemp()
+ tempdirs += [d]
+ pathfile = os.path.join(d, "as_path.svg")
+ tempfiles += [pathfile]
+
+ # Convert all objects (incl. circles) to paths and extract the IDs (not all FreeCAD objects in the SVG have an ID, converting to paths seems to assign them an ID)
+ processAsPath = subprocess.Popen([appImage, sys.argv[-1], '--actions=select-all;object-to-path;select-list', '--export-type=svg', '--export-filename=' + pathfile], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ asPath = processAsPath.communicate()
+ if processAsPath.returncode != 0:
+ # TODO: forward the stderr and use subprocess.CalledProcessError
+ raise ValueError("Child process failed: could not execute Inkscape to convert SVG objects to paths")
+ lines = asPath[0].split(b'\n')
+ if lines[0] == b'Run experimental bundle that bundles everything':
+ lines = lines[1:]
+ ids = [line.split(b' ')[0] for line in lines]
+
+ try:
+ if mountProcess.poll() is None:
+ mountPoint = mountProcess.stdout.readline().rstrip(b'\r\n') # TODO: this is slightly wrong (removes multiple occurrences)
+ else:
+ raise "Could not mount AppImage"
+
+ apprun = os.path.join(mountPoint, b'AppRun')
+ extensionDirectory = os.path.join(mountPoint, b'usr/share/inkscape/extensions')
+ e = os.environb.copy()
+ e['PYTHONPATH'] = e.get('PYTHONPATH', b'') + os.path.pathsep.encode('utf-8') + os.path.join(mountPoint, b'usr/lib/python3/dist-packages/')
+ extensionPy = joinNoDotDot(extensionDirectory, sys.argv[1])
+ cmd = [apprun, 'python3.8', extensionPy] + [b'--id='+id for id in ids] + sys.argv[2:-1] + [pathfile]
+ process = subprocess.Popen(cmd, env=e, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ result = process.communicate()[0]
+ if process.returncode != 0:
+ # TODO: forward the stderr and use subprocess.CalledProcessError
+ raise ValueError("Child process failed: could not execute Inkscape extension with the given parameters")
+ lines = result.split(b'\n')
+ if lines[0] == b'Run experimental bundle that bundles everything':
+ lines = lines[1:]
+ # TODO: use lxml instead
+ with open('/tmp/fooaaa', 'w') as debug:
+ debug.write('trollollo')
+ debug.write('1' + str(lines[0])+'\n\n')
+ lines[0] = re.sub(rb' version="([.0-9]*)"', rb' version="\1" inkscape:version="\1"', lines[0])
+ debug.write('2' + str(lines[0])+'\n\n')
+ #sed -i -e '1 s/ version="1.1"/ inkscape:version="1.1"&/'
+ sys.stdout.buffer.write(b'\n'.join(lines))
+ # --id=rect815 --subdivs 6 --smooth 4.0 /tmp/a.svg 2>/dev/null | sed -e '/Run experimental bundle that bundles everything/,1 d'
+
+ finally:
+ mountProcess.send_signal(signal.SIGINT)
+
+ #LD_LIBRARY_PATH= PYTHONPATH="$extension_directory:$PYTHONPATH" python2 "$extension_directory"/fractalize.py "$@"
+ #/nix/store/zr4xihxd720pq3n0a58ixrqa243hx7an-python-2.7.17-env/bin/python2.7
+
+ #./perso/dl/software/dessin/Inkscape-3bc2e81-x86_64.AppImage --appimage-mount
+finally:
+ for tempfile in tempfiles:
+ try:
+ os.remove(tempfile)
+ except:
+ pass
+ for tempdir in tempdirs:
+ try:
+ os.rmdir(tempdir)
+ except:
+ pass