SVGHMI: Widget Library Picker now transforms SVG widget before allowing DnD. Transform is just identity forn now, but label parsing have already been included. To be continued. svghmi
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Fri, 02 Apr 2021 21:16:18 +0200
branchsvghmi
changeset 3221 3d307ad803ea
parent 3220 ec365ef396b1
child 3222 6adeeb16ac3e
SVGHMI: Widget Library Picker now transforms SVG widget before allowing DnD. Transform is just identity forn now, but label parsing have already been included. To be continued.
svghmi/Makefile
svghmi/gen_dnd_widget_svg.xslt
svghmi/gen_dnd_widget_svg.ysl2
svghmi/hmi_tree.ysl2
svghmi/parse_labels.ysl2
svghmi/ui.py
--- a/svghmi/Makefile	Thu Apr 01 16:00:58 2021 +0200
+++ b/svghmi/Makefile	Fri Apr 02 21:16:18 2021 +0200
@@ -11,7 +11,7 @@
 
 yml2path ?= $(abspath ../../yml2)
 
-ysl2files := gen_index_xhtml.ysl2
+ysl2files := gen_index_xhtml.ysl2 gen_dnd_widget_svg.ysl2
 ysl2includes := $(filter-out $(ysl2files), $(wildcard *.ysl2))
 xsltfiles := $(patsubst %.ysl2, %.xslt, $(ysl2files))
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/gen_dnd_widget_svg.xslt	Fri Apr 02 21:16:18 2021 +0200
@@ -0,0 +1,132 @@
+<?xml version="1.0"?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:str="http://exslt.org/strings" xmlns:func="http://exslt.org/functions" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:ns="beremiz" version="1.0" extension-element-prefixes="ns func exsl regexp str dyn" exclude-result-prefixes="ns func exsl regexp str dyn">
+  <xsl:output method="xml"/>
+  <xsl:variable name="svg" select="/svg:svg"/>
+  <xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/>
+  <xsl:template mode="parselabel" match="*">
+    <xsl:variable name="label" select="@inkscape:label"/>
+    <xsl:variable name="id" select="@id"/>
+    <xsl:variable name="description" select="substring-after($label,'HMI:')"/>
+    <xsl:variable name="_args" select="substring-before($description,'@')"/>
+    <xsl:variable name="args">
+      <xsl:choose>
+        <xsl:when test="$_args">
+          <xsl:value-of select="$_args"/>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:value-of select="$description"/>
+        </xsl:otherwise>
+      </xsl:choose>
+    </xsl:variable>
+    <xsl:variable name="_type" select="substring-before($args,':')"/>
+    <xsl:variable name="type">
+      <xsl:choose>
+        <xsl:when test="$_type">
+          <xsl:value-of select="$_type"/>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:value-of select="$args"/>
+        </xsl:otherwise>
+      </xsl:choose>
+    </xsl:variable>
+    <xsl:if test="$type">
+      <widget>
+        <xsl:attribute name="id">
+          <xsl:value-of select="$id"/>
+        </xsl:attribute>
+        <xsl:attribute name="type">
+          <xsl:value-of select="$type"/>
+        </xsl:attribute>
+        <xsl:for-each select="str:split(substring-after($args, ':'), ':')">
+          <arg>
+            <xsl:attribute name="value">
+              <xsl:value-of select="."/>
+            </xsl:attribute>
+          </arg>
+        </xsl:for-each>
+        <xsl:variable name="paths" select="substring-after($description,'@')"/>
+        <xsl:for-each select="str:split($paths, '@')">
+          <xsl:if test="string-length(.) &gt; 0">
+            <path>
+              <xsl:variable name="pathminmax" select="str:split(.,',')"/>
+              <xsl:variable name="path" select="$pathminmax[1]"/>
+              <xsl:variable name="pathminmaxcount" select="count($pathminmax)"/>
+              <xsl:attribute name="value">
+                <xsl:value-of select="$path"/>
+              </xsl:attribute>
+              <xsl:choose>
+                <xsl:when test="$pathminmaxcount = 3">
+                  <xsl:attribute name="min">
+                    <xsl:value-of select="$pathminmax[2]"/>
+                  </xsl:attribute>
+                  <xsl:attribute name="max">
+                    <xsl:value-of select="$pathminmax[3]"/>
+                  </xsl:attribute>
+                </xsl:when>
+                <xsl:when test="$pathminmaxcount = 2">
+                  <xsl:message terminate="yes">
+                    <xsl:text>Widget id:</xsl:text>
+                    <xsl:value-of select="$id"/>
+                    <xsl:text> label:</xsl:text>
+                    <xsl:value-of select="$label"/>
+                    <xsl:text> has wrong syntax of path section </xsl:text>
+                    <xsl:value-of select="$pathminmax"/>
+                  </xsl:message>
+                </xsl:when>
+              </xsl:choose>
+              <xsl:choose>
+                <xsl:when test="regexp:test($path,'^\.[a-zA-Z0-9_]+$')">
+                  <xsl:attribute name="type">
+                    <xsl:text>PAGE_LOCAL</xsl:text>
+                  </xsl:attribute>
+                </xsl:when>
+                <xsl:when test="regexp:test($path,'^[a-zA-Z0-9_]+$')">
+                  <xsl:attribute name="type">
+                    <xsl:text>HMI_LOCAL</xsl:text>
+                  </xsl:attribute>
+                </xsl:when>
+                <xsl:otherwise>
+                  <xsl:variable name="item" select="$indexed_hmitree/*[@hmipath = $path]"/>
+                  <xsl:variable name="pathtype" select="local-name($item)"/>
+                  <xsl:if test="$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')">
+                    <xsl:message terminate="yes">
+                      <xsl:text>Widget id:</xsl:text>
+                      <xsl:value-of select="$id"/>
+                      <xsl:text> label:</xsl:text>
+                      <xsl:value-of select="$label"/>
+                      <xsl:text> path section </xsl:text>
+                      <xsl:value-of select="$pathminmax"/>
+                      <xsl:text> use min and max on non mumeric value</xsl:text>
+                    </xsl:message>
+                  </xsl:if>
+                  <xsl:if test="count($item) = 1">
+                    <xsl:attribute name="index">
+                      <xsl:value-of select="$item/@index"/>
+                    </xsl:attribute>
+                    <xsl:attribute name="type">
+                      <xsl:value-of select="$pathtype"/>
+                    </xsl:attribute>
+                  </xsl:if>
+                </xsl:otherwise>
+              </xsl:choose>
+            </path>
+          </xsl:if>
+        </xsl:for-each>
+      </widget>
+    </xsl:if>
+  </xsl:template>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="@*">
+    <xsl:copy/>
+  </xsl:template>
+  <xsl:template mode="inline_svg" match="node()">
+    <xsl:copy>
+      <xsl:apply-templates mode="inline_svg" select="@* | node()"/>
+    </xsl:copy>
+  </xsl:template>
+  <xsl:template match="/">
+    <xsl:comment>
+      <xsl:text>Widget dropped in Inkscape from Beremiz</xsl:text>
+    </xsl:comment>
+    <xsl:apply-templates mode="inline_svg" select="/"/>
+  </xsl:template>
+</xsl:stylesheet>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/gen_dnd_widget_svg.ysl2	Fri Apr 02 21:16:18 2021 +0200
@@ -0,0 +1,38 @@
+include yslt_noindent.yml2
+
+in xsl decl svgtmpl(match, xmlns="http://www.w3.org/2000/svg") alias template;
+
+istylesheet
+            /* From Inkscape */
+            xmlns:dc="http://purl.org/dc/elements/1.1/"
+            xmlns:cc="http://creativecommons.org/ns#"
+            xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+            xmlns:svg="http://www.w3.org/2000/svg"
+            xmlns:xlink="http://www.w3.org/1999/xlink"
+            xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+            xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+            xmlns:xhtml="http://www.w3.org/1999/xhtml"
+
+            /* Namespace to invoke python code */
+            xmlns:ns="beremiz"
+
+            extension-element-prefixes="ns func exsl regexp str dyn"
+            exclude-result-prefixes="ns func exsl regexp str dyn" {
+
+    const "svg", "/svg:svg";
+    const "hmi_elements", "//svg:*[starts-with(@inkscape:label, 'HMI:')]";
+
+    include parse_labels.ysl2
+
+    svgtmpl "@*", mode="inline_svg" xsl:copy;
+
+    template "node()", mode="inline_svg" {
+      xsl:copy apply "@* | node()", mode="inline_svg";
+    }
+
+    template "/" {
+        comment > Widget dropped in Inkscape from Beremiz
+
+        apply "/", mode="inline_svg";
+    }
+}
--- a/svghmi/hmi_tree.ysl2	Thu Apr 01 16:00:58 2021 +0200
+++ b/svghmi/hmi_tree.ysl2	Fri Apr 02 21:16:18 2021 +0200
@@ -73,82 +73,7 @@
     }
 }
 
-//  Parses:
-//  "HMI:WidgetType:param1:param2@path1,path1min,path1max@path2"
-//
-//  Into:
-//  widget type="WidgetType" id="blah456" {
-//      arg value="param1";
-//      arg value="param2";
-//      path value=".path1" index=".path1" min="path1min" max="path1max" type="PAGE_LOCAL";
-//      path value="/path1" index="348" type="HMI_INT";
-//      path value="path4" index="path4" type="HMI_LOCAL";
-//  }
-//
-template "*", mode="parselabel" {
-    const "label","@inkscape:label";
-    const "id","@id";
-    const "description", "substring-after($label,'HMI:')";
-
-    const "_args", "substring-before($description,'@')";
-    const "args" choose {
-        when "$_args" value "$_args";
-        otherwise value "$description";
-    }
-
-    const "_type", "substring-before($args,':')";
-    const "type" choose {
-        when "$_type" value "$_type";
-        otherwise value "$args";
-    }
-
-    if "$type" widget {
-        attrib "id" > «$id»
-        attrib "type" > «$type»
-        foreach "str:split(substring-after($args, ':'), ':')" {
-            arg {
-                attrib "value" > «.»
-            }
-        }
-        const "paths", "substring-after($description,'@')";
-        foreach "str:split($paths, '@')" {
-            if "string-length(.) > 0" path {
-                const "pathminmax", "str:split(.,',')";
-                const "path", "$pathminmax[1]";
-                const "pathminmaxcount", "count($pathminmax)";
-                attrib "value" > «$path»
-                choose {
-                    when "$pathminmaxcount = 3" {
-                        attrib "min" > «$pathminmax[2]»
-                        attrib "max" > «$pathminmax[3]»
-                    }
-                    when "$pathminmaxcount = 2" {
-                        error > Widget id:«$id» label:«$label» has wrong syntax of path section «$pathminmax»
-                    }
-                }
-                choose {
-                    when "regexp:test($path,'^\.[a-zA-Z0-9_]+$')" {
-                        attrib "type" > PAGE_LOCAL
-                    }
-                    when "regexp:test($path,'^[a-zA-Z0-9_]+$')" {
-                        attrib "type" > HMI_LOCAL
-                    }
-                    otherwise {
-                        const "item", "$indexed_hmitree/*[@hmipath = $path]";
-                        const "pathtype", "local-name($item)";
-                        if "$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')" {
-                            error > Widget id:«$id» label:«$label» path section «$pathminmax» use min and max on non mumeric value
-                        }
-                        if "count($item) = 1" {
-                            attrib "index" > «$item/@index»
-                            attrib "type" > «$pathtype»
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
+include parse_labels.ysl2
 
 const "_parsed_widgets" {
     widget type="VarInitPersistent" {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/parse_labels.ysl2	Fri Apr 02 21:16:18 2021 +0200
@@ -0,0 +1,80 @@
+// parse_labels.ysl2
+
+
+//  Parses:
+//  "HMI:WidgetType:param1:param2@path1,path1min,path1max@path2"
+//
+//  Into:
+//  widget type="WidgetType" id="blah456" {
+//      arg value="param1";
+//      arg value="param2";
+//      path value=".path1" index=".path1" min="path1min" max="path1max" type="PAGE_LOCAL";
+//      path value="/path1" index="348" type="HMI_INT";
+//      path value="path4" index="path4" type="HMI_LOCAL";
+//  }
+//
+template "*", mode="parselabel" {
+    const "label","@inkscape:label";
+    const "id","@id";
+    const "description", "substring-after($label,'HMI:')";
+
+    const "_args", "substring-before($description,'@')";
+    const "args" choose {
+        when "$_args" value "$_args";
+        otherwise value "$description";
+    }
+
+    const "_type", "substring-before($args,':')";
+    const "type" choose {
+        when "$_type" value "$_type";
+        otherwise value "$args";
+    }
+
+    if "$type" widget {
+        attrib "id" > «$id»
+        attrib "type" > «$type»
+        foreach "str:split(substring-after($args, ':'), ':')" {
+            arg {
+                attrib "value" > «.»
+            }
+        }
+        const "paths", "substring-after($description,'@')";
+        foreach "str:split($paths, '@')" {
+            if "string-length(.) > 0" path {
+                const "pathminmax", "str:split(.,',')";
+                const "path", "$pathminmax[1]";
+                const "pathminmaxcount", "count($pathminmax)";
+                attrib "value" > «$path»
+                choose {
+                    when "$pathminmaxcount = 3" {
+                        attrib "min" > «$pathminmax[2]»
+                        attrib "max" > «$pathminmax[3]»
+                    }
+                    when "$pathminmaxcount = 2" {
+                        error > Widget id:«$id» label:«$label» has wrong syntax of path section «$pathminmax»
+                    }
+                }
+                choose {
+                    when "regexp:test($path,'^\.[a-zA-Z0-9_]+$')" {
+                        attrib "type" > PAGE_LOCAL
+                    }
+                    when "regexp:test($path,'^[a-zA-Z0-9_]+$')" {
+                        attrib "type" > HMI_LOCAL
+                    }
+                    otherwise {
+                        const "item", "$indexed_hmitree/*[@hmipath = $path]";
+                        const "pathtype", "local-name($item)";
+                        if "$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')" {
+                            error > Widget id:«$id» label:«$label» path section «$pathminmax» use min and max on non mumeric value
+                        }
+                        if "count($item) = 1" {
+                            attrib "index" > «$item/@index»
+                            attrib "type" > «$pathtype»
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
--- a/svghmi/ui.py	Thu Apr 01 16:00:58 2021 +0200
+++ b/svghmi/ui.py	Fri Apr 02 21:16:18 2021 +0200
@@ -10,14 +10,22 @@
 import os
 import hashlib
 import weakref
+from tempfile import NamedTemporaryFile
 
 import wx
 
+from lxml import etree
+from lxml.etree import XSLTApplyError
+from XSLTransform import XSLTransform
+
+import util.paths as paths
 from IDEFrame import EncodeFileSystemPath, DecodeFileSystemPath
 from docutil import get_inkscape_path
 
 from util.ProcessLogger import ProcessLogger
 
+ScriptDirectory = paths.AbsDir(__file__)
+
 class HMITreeSelector(wx.TreeCtrl):
     def __init__(self, parent):
         global on_hmitree_update
@@ -35,13 +43,13 @@
                 display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \
                                if c.hmiclass is not None else c.name
                 tc_child = self.AppendItem(current_tc_root, display_name)
-                self.SetPyData(tc_child, None) # TODO
+                self.SetPyData(tc_child, c)
 
                 self._recurseTree(c,tc_child)
             else:
                 display_name = '{} {}'.format(c.nodetype[4:], c.name)
                 tc_child = self.AppendItem(current_tc_root, display_name)
-                self.SetPyData(tc_child, None) # TODO
+                self.SetPyData(tc_child, c)
 
     def MakeTree(self, hmi_tree_root=None):
 
@@ -53,7 +61,7 @@
         root_display_name = _("Please build to see HMI Tree") \
             if hmi_tree_root is None else "HMI"
         self.root = self.AddRoot(root_display_name)
-        self.SetPyData(self.root, None)
+        self.SetPyData(self.root, hmi_tree_root)
 
         if hmi_tree_root is not None:
             self._recurseTree(hmi_tree_root, self.root)
@@ -139,8 +147,7 @@
         sizer.AddGrowableRow(1)
         self.libbutton = wx.Button(self, -1, _("Select SVG widget library"))
         self.widgetpicker = WidgetPicker(self, self.libdir)
-        self.preview = wx.Panel(self, size=(-1, _preview_height + 10))  #, style=wx.SIMPLE_BORDER)
-        #self.preview.SetBackgroundColour(wx.WHITE)
+        self.preview = wx.Panel(self, size=(-1, _preview_height + 10))
         sizer.AddWindow(self.libbutton, flag=wx.GROW)
         sizer.AddWindow(self.widgetpicker, flag=wx.GROW)
         sizer.AddWindow(self.preview, flag=wx.GROW)
@@ -155,6 +162,7 @@
         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
 
         self.msg = _("Drag selected Widget from here to Inkscape")
+        self.tempf = None 
 
     def RecallLibDir(self):
         conf = self.Config.Read(_conf_key)
@@ -248,22 +256,20 @@
             thumbdir = os.path.join(dname, ".svghmithumbs") 
             thumbpath = os.path.join(thumbdir, thumbfname) 
 
-            self.msg = None
             have_thumb = os.path.exists(thumbpath)
 
-            if not have_thumb:
-                try:
+            try:
+                if not have_thumb:
                     if not os.path.exists(thumbdir):
                         os.mkdir(thumbdir)
-                except IOError:
-                    self.msg = _("Widget library must be writable")
-                else:
                     have_thumb = self.GenThumbnail(svgpath, thumbpath)
 
-            self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
-
-            self.selected_SVG = svgpath if have_thumb else None
-            self.ValidateWidget()
+                self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
+
+                self.selected_SVG = svgpath if have_thumb else None
+                self.ValidateWidget()
+            except IOError:
+                self.msg = _("Widget library must be writable")
 
             self.Refresh()
         event.Skip()
@@ -274,23 +280,56 @@
         self.Refresh()
 
     def OnLeftDown(self, evt):
-        if self.selected_SVG is not None:
-            # TODO replace with generated widget file
-            filename = self.selected_SVG
+        if self.tempf is not None:
+            filename = self.tempf.name
             data = wx.FileDataObject()
             data.AddFile(filename)
             dropSource = wx.DropSource(self)
             dropSource.SetData(data)
             dropSource.DoDragDrop(wx.Drag_AllowMove)
 
+    def GiveDetails(self, _context, msgs):
+        for msg in msgs:
+            self.msg += msg+"\n"
+        
     def ValidateWidget(self):
-        if self.selected_SVG is not None:
-            if self.hmitree_node is not None:
-                pass
-        # XXX TODO: 
-        #      - check SVG is valid for selected HMI tree item
-        #      - prepare for D'n'D
-
+        self.msg = ""
+
+        if self.tempf is not None:
+            os.unlink(self.tempf.name)
+            self.tempf = None
+
+        try:
+            if self.selected_SVG is None:
+                raise Exception(_("No widget selected"))
+            if self.hmitree_node is None:
+                raise Exception(_("No HMI tree node selected"))
+
+            transform = XSLTransform(
+                os.path.join(ScriptDirectory, "gen_dnd_widget_svg.xslt"),
+                [("GiveDetails", self.GiveDetails)])
+
+            svgdom = etree.parse(self.selected_SVG)
+
+            result = transform.transform(svgdom) 
+                # hmi_path=self.hmitree_node.path,
+                # hmi_type=self.hmitree_node.nodetype)
+
+            for entry in transform.get_error_log():
+                self.msg += "XSLT: " + entry.message + "\n" 
+
+            self.tempf = NamedTemporaryFile(suffix='.svg', delete=False)
+            result.write(self.tempf, encoding="utf-8")
+            self.tempf.close()
+
+        except Exception as e:
+            self.msg += str(e)
+        except XSLTApplyError as e:
+            self.msg += "Widget transform error: " + e.message
+                
+    def __del__(self):
+        if self.tempf is not None:
+            os.unlink(self.tempf.name)
 
 class SVGHMI_UI(wx.SplitterWindow):
 
@@ -302,6 +341,12 @@
         self.Staging = WidgetLibBrowser(self)
         self.SplitVertically(self.SelectionTree, self.Staging, 300)
         register_for_HMI_tree_updates(weakref.ref(self))
+        self.Bind(wx.EVT_TREE_SEL_CHANGED,
+            self.OnHMITreeNodeSelection, self.SelectionTree)
+
+    def OnHMITreeNodeSelection(self, event):
+        item_pydata = self.SelectionTree.GetPyData(event.GetItem())
+        self.Staging.OnHMITreeNodeSelection(item_pydata)
 
     def HMITreeUpdate(self, hmi_tree_root):
             self.SelectionTree.MakeTree(hmi_tree_root)