From b9b228b8cd330a60434ffe0a30e2d556a494ece3 Mon Sep 17 00:00:00 2001
From: Suzanne Soy <ligo@suzanne.soy>
Date: Thu, 27 May 2021 23:43:55 +0100
Subject: [PATCH] Works for a basic tool that just copies its input shape to
 its output shape.

---
 ToolXML.py                   |   3 +-
 XternalAppsList.py           |   1 +
 XternalAppsParametricTool.py | 334 ++++++++++++++++++++++++++++-------
 myTool.py                    |   6 +-
 myTool.xforms                |  20 ++-
 5 files changed, 286 insertions(+), 78 deletions(-)

diff --git a/ToolXML.py b/ToolXML.py
index 545b432..c0b85a4 100644
--- a/ToolXML.py
+++ b/ToolXML.py
@@ -6,8 +6,7 @@ def getSingletonFromXML(xml, path):
     else:
         return elem
 
-ns={
-  'my':"http://github.com/jsmaniac/XternalApps/myTool",
+ns = {
   'XternalApps':"http://github.com/jsmaniac/XternalApps/v1",
   'xforms':"http://www.w3.org/2002/xforms",
   'xsd':"http://www.w3.org/2001/XMLSchema",
diff --git a/XternalAppsList.py b/XternalAppsList.py
index 56499fd..9350917 100644
--- a/XternalAppsList.py
+++ b/XternalAppsList.py
@@ -75,6 +75,7 @@ apps = Apps(
         extra_xprop_filter = lambda processId, windowId, i: x11prop(windowId, 'WM_STATE',  'WM_STATE') is not None,
         tools = [
             "myTool.xforms"
+            # TODO: generate them from """"inkscape --extension-directory"""
         ]),
     App('GIMP',
         start_command_and_args = ['env', '-i', 'DISPLAY=:0', '/home/suzanne/perso/dotfiles/nix/result/bin/gimp', '--new-instance'],
diff --git a/XternalAppsParametricTool.py b/XternalAppsParametricTool.py
index 291cf33..df02ad1 100644
--- a/XternalAppsParametricTool.py
+++ b/XternalAppsParametricTool.py
@@ -8,12 +8,13 @@ import re
 from copy import deepcopy
 from collections import defaultdict
 import pprint
+import Part
 
 parser = etree.XMLParser(resolve_entities=False)
 
-FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues'])
+FreeCADType = namedtuple('FreeCADType', ['type', 'defaultForType', 'maybeEnumValues', 'maybeMIMEType'])
 
-XFormsInput = namedtuple('XFormsInput', ['modelElementPath', 'label', 'simpleName', 'maybeEnum', 'groupName']) #'type', 'input', 'InputValueToModelValue', 'ModelValueToInputValue'
+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
 
@@ -24,16 +25,17 @@ def CreateCommand(appName, toolName):
     App.ActiveDocument.commitTransaction()
 
 def create(appName, toolName):
+    sel = FreeCADGui.Selection.getSelection()
     name = appName + toolName
     obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", name)
-    XternalAppsParametricTool(obj, appName, toolName)
+    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),
-    'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None),
+    'xsd:decimal': FreeCADType(type='App::PropertyFloat', defaultForType=0.0, maybeEnumValues=None, maybeMIMEType=None),
+    'xsd:string': FreeCADType(type='App::PropertyString', defaultForType='', maybeEnumValues=None, maybeMIMEType=None),
 }
 
 def getShortPath(root, elem, root_needs_dot = True):
@@ -80,33 +82,16 @@ 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', maybeEnumValues = enumValues, defaultForType = (enumValues[0] if len(enumValues) > 0 else None))
+        return FreeCADType(type = 'App::PropertyEnumeration', defaultForType = (enumValues[0] if len(enumValues) > 0 else None), maybeEnumValues = enumValues, maybeMIMEType=None)
     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)
+        return FreeCADType(type='App::PropertyLink', defaultForType=None, maybeEnumValues=None, maybeMIMEType = MIMEType)
     else:
         raise ValueError('Unsupported MIME type')
 
-def encode_bytes(bytestring):
-    try:
-        return ("utf-8", bytestring.decode('utf-8', errors='strict'))
-    except ValueError:
-        from base64 import b64encode
-        return ("base64", b64encode(bytestring))
-
-def decode_bytes(encoding_and_string):
-    encoding, string = encoding_and_string
-    if encoding == "utf-8":
-        return string.encode('utf-8')
-    elif encoding == "base64":
-        from base64 import b64decode
-        return b64decode(string)
-    else:
-        raise ValueError("invalid encoding: expected utf-8 or base64")
-
 def toSimpleName(name):
     return re.sub(r'( |[^-a-zA-Z0-9])+', ' ', name).title().replace(' ', '')
 
@@ -126,8 +111,27 @@ def toUniqueSimpleName(name, mutableNextUnique):
         mutableNextUnique[name] = mutableNextUnique[name] + 1
     return base + counter
 
+def lookup(dict, part, value):
+    kvs = [(k, v) for k, v in dict.items() if part(v) == value]
+    if len(kvs) == 1:
+        return kvs[0]
+    return (None, None)
+
+def exportSVG(obj, svgfile):
+    import importSVG
+    p=App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
+    old_svg_export_style = p.GetInt('svg_export_style')
+    try:
+        p.SetInt('svg_export_style', 1)
+        importSVG.export([obj], svgfile)
+    finally:
+        p.SetInt('svg_export_style', old_svg_export_style)
+    
+    # TODO: modify the SVG to set a fake Inkscape version, to avoid the pop-up dialog.
+
+
 class XternalAppsParametricTool():
-    def __init__(self, obj, appName, toolName):
+    def __init__(self, obj, appName, toolName, sel=[]):
         self.Type = "XternalAppsParametricTool"
         self.AppName = appName
         self.ToolName = toolName        
@@ -141,7 +145,28 @@ class XternalAppsParametricTool():
         self.ModelInstance = self.defaults
         self.createProperties(obj, self.types, self.ModelInstance, self.form)
 
-        self.MonitorChanges = True;
+        self.oldExpressionEngine = obj.ExpressionEngine
+
+        self.MonitorChanges = True
+
+        #pprint.pprint(self.types)
+        #pprint.pprint(self.form)
+
+        # Special treatment for the "primary" form field
+        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
+
+            # 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')
+
+            if type == 'App::PropertyLink' and len(sel) >= 1:
+                setattr(obj, primary.simpleName, sel[0])
+            elif type == 'App::PropertyLinkList':
+                setattr(obj, primary.simpleName, sel)
+
         return
 
     def __getstate__(self):
@@ -160,53 +185,61 @@ class XternalAppsParametricTool():
             self.defaults = self.xmlDefaultsToPython(self.Tool.XForms, self.types)
             self.form     = self.xmlFormToPython(self.Tool.XForms, self.types)
             
-    def getDefaultModelInstance(self, obj):
-        xml = etree.parse(self.Tool.XForms, parser=parser)
-        model = xml.find('./xforms:model', ns)
-        instanceDocument = etree.ElementTree(model.find('./xforms:instance/*', ns))
-        return deepcopy(instanceDocument)
-
-    def typecheckModelInstance(self, types, instance):
-        """TODO"""
-
-    def updateModelInstance(self, instance, ref, value):
-        """TODO"""
-
     def onChanged(self, obj, prop):
+        import sys
+        #print ('onChanged' + str(prop), file=sys.stderr)
+        
+        restoreMonitorChanges = self.MonitorChanges
         if self.MonitorChanges:
             try:
+                #print('MonitorChanges = False')
                 self.MonitorChanges = False
-                inputs = [input for input in self.form.values() if input.simpleName == prop]
-                if len(inputs) == 1:
-                    input = inputs[0]
+                # clear expressions attached to the same part of the model
+                if (prop == 'ExpressionEngine'):
+                    # compare with oldExpressionEngine
+                    added = []
+                    removed = []
+                    for (k, _) in obj.ExpressionEngine:
+                        if k not in self.oldExpressionEngine:
+                            added = added + [k]
+                    #for k in self.oldExpressionEngine:
+                    #    if k not in self.oldExpressionEngine:
+                    #        removed = removed + [k]
+                    # Clear expressions for properties linked to the one that was added.
+                    for a in added:
+                        _, input = lookup(self.form, lambda input: input.simpleName, a)
+                        if input:
+                            for other in self.form.values():
+                                if other.modelElementPath == input.modelElementPath and other.simpleName != input.simpleName:
+                                    # clear other.simpleName
+                                    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)
-                            obj.setExpression(other.simpleName, None)
             finally:
-                self.MonitorChanges = True
-        return
-        
-        if hasattr(self, 'SimpleInputNameToInput'):
-            inputPath = self.SimpleInputNameToInput.get(prop, None)
-            if inputPath is not None:
-                modelElementPath = self.Inputs[inputPath].modelElementPath
-                modelElement = self.ModelInstance.find(modelElementPath)
-                newText = self.Inputs[inputPath].InputValueToModelValue(getattr(obj, prop))
-                print((prop, getattr(obj, prop), modelElement.text, newText))
-                if modelElement.text != newText:
-                    modelElement.text = newText
-                    for inputPathToUpdate in self.ModelOnChanged[modelElementPath]:
-                        if inputPathToUpdate != inputPath:
-                            # TODO: this is terrible and will lead to infinite update loops
-                            setattr(obj, self.InputToSimpleInputName[inputPathToUpdate], self.Inputs[inputPathToUpdate].ModelValueToInputValue(newText))
+                #print('MonitorChanges = ' + str(restoreMonitorChanges))
+                self.MonitorChanges = restoreMonitorChanges
 
     def interpretFormElement(self, xmlXFormsElement, xml, instanceDocument, types):
         # TODO: is it safe to pass input unprotected here?
@@ -220,8 +253,15 @@ class XternalAppsParametricTool():
         if type is None:
             raise Exception('Could not find type for ' + modelElementPath)
         path = xml.getelementpath(xmlXFormsElement)
-        path = getShortPath(xml, xmlXFormsElement) # YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
-        return (path, xmlXFormsElement, modelElementPath, type)
+        path = getShortPath(xml, xmlXFormsElement) # TODO !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+        
+        relevance = xmlXFormsElement.xpath('./@XternalApps:relevance', namespaces=ns)
+        if len(relevance):
+            relevance = relevance[0]
+        else:
+            relevance = None
+        
+        return (path, xmlXFormsElement, modelElementPath, type, relevance)
 
     def xmlFormToPython(self, form_xml, types):
         """Parse the …-form.xml document, and return
@@ -236,16 +276,31 @@ class XternalAppsParametricTool():
         # register all inputs to inputs[pathToElement]
         for group in xml.findall('./xforms:group', ns):
             for input in group.findall('./xforms:input', ns):
-                path, xmlXFormsElement, modelElementPath, _type = self.interpretFormElement(input, xml, instanceDocument, types)
+                path, xmlXFormsElement, modelElementPath, _type, relevance = self.interpretFormElement(input, xml, instanceDocument, types)
 
                 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,
+                inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=None, groupName=groupName, relevance=relevance) # type=type,
+            
+            for upload in group.findall("./xforms:upload", ns):
+                path, xmlXFormsElement, modelElementPath, _type, relevance = self.interpretFormElement(upload, xml, instanceDocument, types)
+
+                label = upload.xpath('./xforms:label/text()', namespaces=ns)
+                if len(label) != 1:
+                    raise ValueError("An xforms:upload element should contain exactly one xforms:label element")
+                label = label[0]
+                if upload.xpath('./xforms:filename/@ref', namespaces=ns) != ['@filename']:
+                    raise ValueError("The ref attribute of an xforms:filename should always be the string '@filename' (this is a limitation of the XternalApps format).")
+
+                simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
+                groupName = "/".join(upload.xpath('ancestor-or-self::xforms:group/xforms:label/text()', namespaces=ns)) or None
+
+                inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=None, groupName=groupName, relevance=relevance) # type=type,
+
             for select1 in group.findall('./xforms:select1', ns):
-                path, xmlXFormsElement, modelElementPath, _type = self.interpretFormElement(select1, xml, instanceDocument, types)
+                path, xmlXFormsElement, modelElementPath, _type, relevance = self.interpretFormElement(select1, xml, instanceDocument, types)
                 
                 label = select1.attrib['label']
                 simpleName = toUniqueSimpleName(toSimpleName(label), nextUniqueSimpleName)
@@ -256,7 +311,7 @@ class XternalAppsParametricTool():
                 for item in select1.findall('./xforms:item', ns):
                   enum[item.attrib['label']] = item.attrib['value']
                 # input=xmlXFormsElement,
-                inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=enum, groupName=groupName) # type=type,
+                inputs[path] = XFormsInput(modelElementPath=modelElementPath, label=label, simpleName=simpleName, maybeEnum=enum, groupName=groupName, relevance=relevance) # type=type,
         
         return inputs
 
@@ -297,7 +352,7 @@ class XternalAppsParametricTool():
         return defaults
 
     def createProperties(self, obj, types, defaults, form):
-        for key, (modelElementPath, label, simpleName, maybeEnum, groupName) in form.items():
+        for key, (modelElementPath, label, simpleName, maybeEnum, groupName, relevance) in form.items():
             obj.addProperty(types[modelElementPath].type,
                             simpleName,
                             groupName,
@@ -305,7 +360,6 @@ class XternalAppsParametricTool():
             default = defaults[modelElementPath]
             if maybeEnum is not None:
                 setattr(obj, simpleName, list(maybeEnum.keys()))
-                print(default, maybeEnum)
                 # TODO: use a bidirectional dict
                 default = [k for k, v in maybeEnum.items() if v == default][0]
             setattr(obj, simpleName, default)
@@ -314,5 +368,149 @@ class XternalAppsParametricTool():
     def Tool(self):
         return XternalAppsList.apps[self.AppName].Tools[self.ToolName]
 
+    def xmlCommandToPython(self, document):
+        """Parse the .xml document, and return
+        a pair of dictionaries accepts[model_path] = style and returns[model_path] = style."""
+
+        xml = etree.parse(self.Tool.XForms, parser=parser)
+
+        model_root = xml.find('./xforms:model', ns)
+        instanceDocument = etree.ElementTree(model_root.find('./xforms:instance/*', ns))
+
+        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 = {}
+        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 = []
+        tempfiles = []
+        tempdirs = []
+        for modelElementPath, value in self.ModelInstance.items():
+            style = styles[modelElementPath]
+            type = self.types[modelElementPath]
+            
+            # 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)
+
+            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
+        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([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
+
+        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)
+
+        print(exitcode, result)
+
     def execute(self, obj):
-        """This is called when the object is recomputed"""
+        print("""This is called when the object is recomputed""")
+        self.xmlCommandToPython(obj.Document)
+
+    #<XternalApps:accepts>
+    #  <XternalApps:default ref=".//*" style="double-dash">
+    #  <XternalApps:exception ref="my:svgfile" style="pipe" />
+    #</XternalApps:accepts>
+
+    #<XternalApps:returns>
+    #  <XternalApps:exception ref="my:output-svgfile" style="pipe" />
+    #  <XternalApps:exception ref="my:output-exitcode" style="exitcode" />
+    #</XternalApps:returns>
+
+
diff --git a/myTool.py b/myTool.py
index bc68f82..bfa5312 100755
--- a/myTool.py
+++ b/myTool.py
@@ -2,5 +2,7 @@
 
 import sys
 
-for arg in sys.argv:
-  print(str(arg)
+#for arg in sys.argv:
+#  print('<!--' + str(arg) + '-->')
+
+sys.stdout.write(sys.stdin.read())
diff --git a/myTool.xforms b/myTool.xforms
index d4d6422..85269a2 100644
--- a/myTool.xforms
+++ b/myTool.xforms
@@ -11,7 +11,7 @@
   <xforms:model>
     <xforms:instance>
       <my:tool>
-        <my:svgfile/>
+        <my:svgfile filename="" />
         <my:option1>default value</my:option1>
         <my:option2/>
         <my:option3>bar</my:option3>
@@ -26,10 +26,6 @@
     <xforms:bind ref="my:option2" type="xsd:decimal" required="true()"/>
     <xforms:bind ref="my:option3" type="my:enum-option3" required="true()"/>
     <!--<xforms:submission action="myTool.py" method="exec-double-dash" />-->
-    <XternalApps:command medhod="exec" style="double-dash">
-      <XternalApps:exception ref="my:svgfile" style="pipe" />
-      <XternalApps:returns style="pipe" type="image/svg+xml" />
-    </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">
@@ -39,12 +35,24 @@
       </xsd:simpleType>
     </xsd:schema>
   </xforms:model>
+
+  <XternalApps:command method="exec" name="/home/suzanne/perso/projects/paper-craft/py/Mod/XternalApps/myTool.py">
+    <XternalApps:accepts>
+      <XternalApps:default style="double-dash" />
+      <XternalApps:exception ref="my:svgfile" style="pipe" />
+    </XternalApps:accepts>
+    <XternalApps:returns>
+      <XternalApps:exception ref="my:output-svgfile" style="pipe" />
+      <XternalApps:exception ref="my:output-exitcode" style="exitcode" />
+    </XternalApps:returns>
+  </XternalApps:command>
+
   <!-- Description of the user interface follows: -->
   <xforms:group>
     <xforms:label>Page 1</xforms:label>
     <xforms:input ref="my:option1" label="Option One ∀"/>
     <xforms:input ref="my:option2" label="Option Two π"/>
-    <xforms:upload ref="my:svgfile" accept="image/svg+xml">
+    <xforms:upload ref="my:svgfile" accept="image/svg+xml" XternalApps:relevance="primary">
       <xforms:label>Input image</xforms:label>
       <xforms:filename ref="@filename" />
     </xforms:upload>