# HG changeset patch # User Edouard Tisserant # Date 1617390978 -7200 # Node ID 3d307ad803eab8c89679d854b5d30f229aea7a14 # Parent ec365ef396b1ff3facfe5cece44904890751de08 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. diff -r ec365ef396b1 -r 3d307ad803ea svghmi/Makefile --- 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)) diff -r ec365ef396b1 -r 3d307ad803ea svghmi/gen_dnd_widget_svg.xslt --- /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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Widget id: + + label: + + has wrong syntax of path section + + + + + + + + PAGE_LOCAL + + + + + HMI_LOCAL + + + + + + + + Widget id: + + label: + + path section + + use min and max on non mumeric value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Widget dropped in Inkscape from Beremiz + + + + diff -r ec365ef396b1 -r 3d307ad803ea svghmi/gen_dnd_widget_svg.ysl2 --- /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"; + } +} diff -r ec365ef396b1 -r 3d307ad803ea svghmi/hmi_tree.ysl2 --- 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" { diff -r ec365ef396b1 -r 3d307ad803ea svghmi/parse_labels.ysl2 --- /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» + } + } + } + } + } + } +} + diff -r ec365ef396b1 -r 3d307ad803ea svghmi/ui.py --- 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)