svghmi/ui.py
changeset 3302 c89fc366bebd
parent 3279 5615e062a77d
child 3303 0ffb41625592
child 3454 0b5ab53007a9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/ui.py	Thu Sep 02 21:36:29 2021 +0200
@@ -0,0 +1,700 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz
+# Copyright (C) 2021: Edouard TISSERANT
+#
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import os
+import hashlib
+import weakref
+import re
+from threading import Thread, Lock
+from functools import reduce
+from itertools import izip
+from operator import or_
+from tempfile import NamedTemporaryFile
+
+import wx
+from wx.lib.scrolledpanel import ScrolledPanel
+
+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__)
+
+HMITreeDndMagicWord = "text/beremiz-hmitree"
+
+class HMITreeSelector(wx.TreeCtrl):
+    def __init__(self, parent):
+
+        wx.TreeCtrl.__init__(self, parent, style=(
+            wx.TR_MULTIPLE |
+            wx.TR_HAS_BUTTONS |
+            wx.SUNKEN_BORDER |
+            wx.TR_LINES_AT_ROOT))
+
+        self.ordered_items = []
+        self.parent = parent
+
+        self.MakeTree()
+
+        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeNodeSelection)
+        self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag)
+
+    def _recurseTree(self, current_hmitree_root, current_tc_root):
+        for c in current_hmitree_root.children:
+            if hasattr(c, "children"):
+                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, 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, c)
+
+    def OnTreeNodeSelection(self, event):
+        items = self.GetSelections()
+        items_pydata = [self.GetPyData(item) for item in items]
+
+        # append new items to ordered item list
+        for item_pydata in items_pydata:
+            if item_pydata not in self.ordered_items:
+                self.ordered_items.append(item_pydata)
+
+        # filter out vanished items
+        self.ordered_items = [
+            item_pydata 
+            for item_pydata in self.ordered_items 
+            if item_pydata in items_pydata]
+
+        self.parent.OnHMITreeNodeSelection(self.ordered_items)
+
+    def OnTreeBeginDrag(self, event):
+        """
+        Called when a drag is started in tree
+        @param event: wx.TreeEvent
+        """
+        if self.ordered_items:
+            # Just send a recognizable mime-type, drop destination
+            # will get python data from parent
+            data = wx.CustomDataObject(HMITreeDndMagicWord)
+            dragSource = wx.DropSource(self)
+            dragSource.SetData(data)
+            dragSource.DoDragDrop()
+
+    def MakeTree(self, hmi_tree_root=None):
+
+        self.Freeze()
+
+        self.root = None
+        self.DeleteAllItems()
+
+        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, hmi_tree_root)
+
+        if hmi_tree_root is not None:
+            self._recurseTree(hmi_tree_root, self.root)
+            self.Expand(self.root)
+
+        self.Thaw()
+
+class WidgetPicker(wx.TreeCtrl):
+    def __init__(self, parent, initialdir=None):
+        wx.TreeCtrl.__init__(self, parent, style=(
+            wx.TR_MULTIPLE |
+            wx.TR_HAS_BUTTONS |
+            wx.SUNKEN_BORDER |
+            wx.TR_LINES_AT_ROOT))
+
+        self.MakeTree(initialdir)
+
+    def _recurseTree(self, current_dir, current_tc_root, dirlist):
+        """
+        recurse through subdirectories, but creates tree nodes 
+        only when (sub)directory conbtains .svg file
+        """
+        res = []
+        for f in sorted(os.listdir(current_dir)):
+            p = os.path.join(current_dir,f)
+            if os.path.isdir(p):
+
+                r = self._recurseTree(p, current_tc_root, dirlist + [f])
+                if len(r) > 0 :
+                    res = r
+                    dirlist = []
+                    current_tc_root = res.pop()
+
+            elif os.path.splitext(f)[1].upper() == ".SVG":
+                if len(dirlist) > 0 :
+                    res = []
+                    for d in dirlist:
+                        current_tc_root = self.AppendItem(current_tc_root, d)
+                        res.append(current_tc_root)
+                        self.SetPyData(current_tc_root, None)
+                    dirlist = []
+                    res.pop()
+                tc_child = self.AppendItem(current_tc_root, f)
+                self.SetPyData(tc_child, p)
+        return res
+
+    def MakeTree(self, lib_dir = None):
+
+        self.Freeze()
+
+        self.root = None
+        self.DeleteAllItems()
+
+        root_display_name = _("Please select widget library directory") \
+            if lib_dir is None else os.path.basename(lib_dir)
+        self.root = self.AddRoot(root_display_name)
+        self.SetPyData(self.root, None)
+
+        if lib_dir is not None and os.path.exists(lib_dir):
+            self._recurseTree(lib_dir, self.root, [])
+            self.Expand(self.root)
+
+        self.Thaw()
+
+class PathDropTarget(wx.DropTarget):
+
+    def __init__(self, parent):
+        data = wx.CustomDataObject(HMITreeDndMagicWord)
+        wx.DropTarget.__init__(self, data)
+        self.ParentWindow = parent
+
+    def OnDrop(self, x, y):
+        self.ParentWindow.OnHMITreeDnD()
+        return True
+
+class ParamEditor(wx.Panel):
+    def __init__(self, parent, paramdesc):
+        wx.Panel.__init__(self, parent.main_panel)
+        label = paramdesc.get("name")+ ": " + paramdesc.get("accepts") 
+        if paramdesc.text:
+            label += "\n\"" + paramdesc.text + "\""
+        self.desc = wx.StaticText(self, label=label)
+        self.valid_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK, wx.ART_TOOLBAR, (16,16))
+        self.invalid_bmp = wx.ArtProvider.GetBitmap(wx.ART_CROSS_MARK, wx.ART_TOOLBAR, (16,16))
+        self.validity_sbmp = wx.StaticBitmap(self, -1, self.invalid_bmp)
+        self.edit = wx.TextCtrl(self)
+        self.edit_sizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0)
+        self.edit_sizer.AddGrowableCol(0)
+        self.edit_sizer.AddGrowableRow(0)
+        self.edit_sizer.Add(self.edit, flag=wx.GROW)
+        self.edit_sizer.Add(self.validity_sbmp, flag=wx.GROW)
+        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.main_sizer.Add(self.desc, flag=wx.GROW)
+        self.main_sizer.Add(self.edit_sizer, flag=wx.GROW)
+        self.SetSizer(self.main_sizer)
+        self.main_sizer.Fit(self)
+
+    def GetValue(self):
+        return self.edit.GetValue()
+
+    def setValidity(self, validity):
+        if validity is not None:
+            bmp = self.valid_bmp if validity else self.invalid_bmp
+            self.validity_sbmp.SetBitmap(bmp)
+            self.validity_sbmp.Show(True)
+        else :
+            self.validity_sbmp.Show(False)
+
+models = { typename: re.compile(regex) for typename, regex in [
+    ("string", r".*"),
+    ("int", r"^-?([1-9][0-9]*|0)$"),
+    ("real", r"^-?([1-9][0-9]*|0)(\.[0-9]+)?$")]}
+
+class ArgEditor(ParamEditor):
+    def __init__(self, parent, argdesc, prefillargdesc):
+        ParamEditor.__init__(self, parent, argdesc)
+        self.ParentObj = parent
+        self.argdesc = argdesc
+        self.Bind(wx.EVT_TEXT, self.OnArgChanged, self.edit)
+        prefill = "" if prefillargdesc is None else prefillargdesc.get("value")
+        self.edit.SetValue(prefill)
+        # TODO add a button to add more ArgEditror instance 
+        #      when ordinality is multiple
+
+    def OnArgChanged(self, event):
+        txt = self.edit.GetValue()
+        accepts = self.argdesc.get("accepts").split(',')
+        self.setValidity(
+            reduce(or_,
+                   map(lambda typename: 
+                           models[typename].match(txt) is not None,
+                       accepts), 
+                   False)
+            if accepts and txt else None)
+        self.ParentObj.RegenSVGLater()
+        event.Skip()
+
+class PathEditor(ParamEditor):
+    def __init__(self, parent, pathdesc):
+        ParamEditor.__init__(self, parent, pathdesc)
+        self.ParentObj = parent
+        self.pathdesc = pathdesc
+        DropTarget = PathDropTarget(self)
+        self.edit.SetDropTarget(DropTarget)
+        self.edit.SetHint(_("Drag'n'drop HMI variable here"))
+        self.Bind(wx.EVT_TEXT, self.OnPathChanged, self.edit)
+
+    def OnHMITreeDnD(self):
+        self.ParentObj.GotPathDnDOn(self)
+
+    def SetPath(self, hmitree_node):
+        self.edit.ChangeValue(hmitree_node.hmi_path())
+        self.setValidity(
+            hmitree_node.nodetype in self.pathdesc.get("accepts").split(","))
+
+    def OnPathChanged(self, event):
+        # TODO : find corresponding hmitre node and type to update validity
+        # Lazy way : hide validity
+        self.setValidity(None)
+        self.ParentObj.RegenSVGLater()
+        event.Skip()
+    
+def KeepDoubleNewLines(txt):
+    return "\n\n".join(map(
+        lambda s:re.sub(r'\s+',' ',s),
+        txt.split("\n\n")))
+
+_conf_key = "SVGHMIWidgetLib"
+_preview_height = 200
+_preview_margin = 5
+class WidgetLibBrowser(wx.SplitterWindow):
+    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
+                 size=wx.DefaultSize):
+
+        wx.SplitterWindow.__init__(self, parent,
+                                   style=wx.SUNKEN_BORDER | wx.SP_3D)
+
+        self.bmp = None
+        self.msg = None
+        self.hmitree_nodes = []
+        self.selected_SVG = None
+
+        self.Config = wx.ConfigBase.Get()
+        self.libdir = self.RecallLibDir()
+        if self.libdir is None:
+            self.libdir = os.path.join(ScriptDirectory, "widgetlib") 
+
+        self.picker_desc_splitter = wx.SplitterWindow(self, style=wx.SUNKEN_BORDER | wx.SP_3D)
+
+        self.picker_panel = wx.Panel(self.picker_desc_splitter)
+        self.picker_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
+        self.picker_sizer.AddGrowableCol(0)
+        self.picker_sizer.AddGrowableRow(1)
+
+        self.widgetpicker = WidgetPicker(self.picker_panel, self.libdir)
+        self.libbutton = wx.Button(self.picker_panel, -1, _("Select SVG widget library"))
+
+        self.picker_sizer.Add(self.libbutton, flag=wx.GROW)
+        self.picker_sizer.Add(self.widgetpicker, flag=wx.GROW)
+        self.picker_sizer.Layout()
+        self.picker_panel.SetAutoLayout(True)
+        self.picker_panel.SetSizer(self.picker_sizer)
+
+        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
+        self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton)
+
+
+
+        self.main_panel = ScrolledPanel(parent=self,
+                                                name='MiscellaneousPanel',
+                                                style=wx.TAB_TRAVERSAL)
+
+        self.main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0)
+        self.main_sizer.AddGrowableCol(0)
+        self.main_sizer.AddGrowableRow(2)
+
+        self.staticmsg = wx.StaticText(self, label = _("Drag selected Widget from here to Inkscape"))
+        self.preview = wx.Panel(self.main_panel, size=(-1, _preview_height + _preview_margin*2))
+        self.signature_sizer = wx.BoxSizer(wx.VERTICAL)
+        self.args_box = wx.StaticBox(self.main_panel, -1,
+                                     _("Widget's arguments"),
+                                     style = wx.ALIGN_CENTRE_HORIZONTAL)
+        self.args_sizer = wx.StaticBoxSizer(self.args_box, wx.VERTICAL)
+        self.paths_box = wx.StaticBox(self.main_panel, -1,
+                                      _("Widget's variables"),
+                                      style = wx.ALIGN_CENTRE_HORIZONTAL)
+        self.paths_sizer = wx.StaticBoxSizer(self.paths_box, wx.VERTICAL)
+        self.signature_sizer.Add(self.args_sizer, flag=wx.GROW)
+        self.signature_sizer.AddSpacer(5)
+        self.signature_sizer.Add(self.paths_sizer, flag=wx.GROW)
+        self.main_sizer.Add(self.staticmsg, flag=wx.GROW)
+        self.main_sizer.Add(self.preview, flag=wx.GROW)
+        self.main_sizer.Add(self.signature_sizer, flag=wx.GROW)
+        self.main_sizer.Layout()
+        self.main_panel.SetAutoLayout(True)
+        self.main_panel.SetSizer(self.main_sizer)
+        self.main_sizer.Fit(self.main_panel)
+        self.preview.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.preview.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
+
+        self.desc = wx.TextCtrl(self.picker_desc_splitter, size=wx.Size(-1, 160),
+                                   style=wx.TE_READONLY | wx.TE_MULTILINE)
+
+        self.picker_desc_splitter.SplitHorizontally(self.picker_panel, self.desc, 400)
+        self.SplitVertically(self.main_panel, self.picker_desc_splitter, 300)
+
+        self.tempf = None 
+
+        self.RegenSVGThread = None
+        self.RegenSVGLock = Lock()
+        self.RegenSVGTimer = wx.Timer(self, -1)
+        self.RegenSVGParams = None
+        self.Bind(wx.EVT_TIMER,
+                  self.RegenSVG,
+                  self.RegenSVGTimer)
+
+        self.args_editors = []
+        self.paths_editors = []
+
+    def SetMessage(self, msg):
+        self.staticmsg.SetLabel(msg)
+        self.main_sizer.Layout()
+
+    def ResetSignature(self):
+        self.args_sizer.Clear()
+        for editor in self.args_editors:
+            editor.Destroy()
+        self.args_editors = []
+
+        self.paths_sizer.Clear()
+        for editor in self.paths_editors:
+            editor.Destroy()
+        self.paths_editors = []
+
+    def AddArgToSignature(self, arg, prefillarg):
+        new_editor = ArgEditor(self, arg, prefillarg)
+        self.args_editors.append(new_editor)
+        self.args_sizer.Add(new_editor, flag=wx.GROW)
+
+    def AddPathToSignature(self, path):
+        new_editor = PathEditor(self, path)
+        self.paths_editors.append(new_editor)
+        self.paths_sizer.Add(new_editor, flag=wx.GROW)
+
+    def GotPathDnDOn(self, target_editor):
+        dndindex = self.paths_editors.index(target_editor)
+
+        for hmitree_node,editor in zip(self.hmitree_nodes,
+                                   self.paths_editors[dndindex:]):
+            editor.SetPath(hmitree_node)
+
+        self.RegenSVGNow()
+
+    def RecallLibDir(self):
+        conf = self.Config.Read(_conf_key)
+        if len(conf) == 0:
+            return None
+        else:
+            return DecodeFileSystemPath(conf)
+
+    def RememberLibDir(self, path):
+        self.Config.Write(_conf_key,
+                          EncodeFileSystemPath(path))
+        self.Config.Flush()
+
+    def DrawPreview(self):
+        """
+        Refresh preview panel 
+        """
+        # Init preview panel paint device context
+        dc = wx.PaintDC(self.preview)
+        dc.Clear()
+
+        if self.bmp:
+            # Get Preview panel size
+            sz = self.preview.GetClientSize()
+            w = self.bmp.GetWidth()
+            dc.DrawBitmap(self.bmp, (sz.width - w)/2, _preview_margin)
+
+
+
+    def OnSelectLibDir(self, event):
+        defaultpath = self.RecallLibDir()
+        if defaultpath == None:
+            defaultpath = os.path.expanduser("~")
+
+        dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath,
+                              style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
+
+        if dialog.ShowModal() == wx.ID_OK:
+            self.libdir = dialog.GetPath()
+            self.RememberLibDir(self.libdir)
+            self.widgetpicker.MakeTree(self.libdir)
+
+        dialog.Destroy()
+
+    def OnPaint(self, event):
+        """
+        Called when Preview panel needs to be redrawn
+        @param event: wx.PaintEvent
+        """
+        self.DrawPreview()
+        event.Skip()
+
+    def GenThumbnail(self, svgpath, thumbpath):
+        inkpath = get_inkscape_path()
+        if inkpath is None:
+            self.msg = _("Inkscape is not installed.")
+            return False
+        # TODO: spawn a thread, to decouple thumbnail gen
+        status, result, _err_result = ProcessLogger(
+            None,
+            '"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath +
+            '" -D -h ' + str(_preview_height)).spin()
+        if status != 0:
+            self.msg = _("Inkscape couldn't generate thumbnail.")
+            return False
+        return True
+
+    def OnWidgetSelection(self, event):
+        """
+        Called when tree item is selected
+        @param event: wx.TreeEvent
+        """
+        item_pydata = self.widgetpicker.GetPyData(event.GetItem())
+        if item_pydata is not None:
+            svgpath = item_pydata
+            dname = os.path.dirname(svgpath)
+            fname = os.path.basename(svgpath)
+            hasher = hashlib.new('md5')
+            with open(svgpath, 'rb') as afile:
+                while True:
+                    buf = afile.read(65536)
+                    if len(buf) > 0:
+                        hasher.update(buf)
+                    else:
+                        break
+            digest = hasher.hexdigest()
+            thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png"
+            thumbdir = os.path.join(dname, ".svghmithumbs") 
+            thumbpath = os.path.join(thumbdir, thumbfname) 
+
+            have_thumb = os.path.exists(thumbpath)
+
+            try:
+                if not have_thumb:
+                    if not os.path.exists(thumbdir):
+                        os.mkdir(thumbdir)
+                    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.AnalyseWidgetAndUpdateUI(fname)
+
+                self.SetMessage(self.msg)
+
+            except IOError:
+                self.msg = _("Widget library must be writable")
+
+            self.Refresh()
+        event.Skip()
+
+    def OnHMITreeNodeSelection(self, hmitree_nodes):
+        self.hmitree_nodes = hmitree_nodes
+
+    def OnLeftDown(self, evt):
+        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 RegenSVGLater(self, when=1):
+        self.SetMessage(_("SVG generation pending"))
+        self.RegenSVGTimer.Start(milliseconds=when*1000, oneShot=True)
+
+    def RegenSVGNow(self):
+        self.RegenSVGLater(when=0)
+
+    def RegenSVG(self, event):
+        self.SetMessage(_("Generating SVG..."))
+        args = [arged.GetValue() for arged in self.args_editors]
+        while args and not args[-1]: args.pop(-1)
+        paths = [pathed.GetValue() for pathed in self.paths_editors]
+        while paths and not paths[-1]: paths.pop(-1)
+        if self.RegenSVGLock.acquire(True):
+            self.RegenSVGParams = (args, paths)
+            if self.RegenSVGThread is None:
+                self.RegenSVGThread = \
+                    Thread(target=self.RegenSVGProc,
+                           name="RegenSVGThread").start()
+            self.RegenSVGLock.release()
+        event.Skip()
+
+    def RegenSVGProc(self):
+        self.RegenSVGLock.acquire(True)
+
+        newparams = self.RegenSVGParams
+        self.RegenSVGParams = None
+
+        while newparams is not None:
+            self.RegenSVGLock.release()
+
+            res = self.GenDnDSVG(newparams)
+
+            self.RegenSVGLock.acquire(True)
+
+            newparams = self.RegenSVGParams
+            self.RegenSVGParams = None
+
+        self.RegenSVGThread = None
+
+        self.RegenSVGLock.release()
+
+        wx.CallAfter(self.DoneRegenSVG)
+        
+    def DoneRegenSVG(self):
+        self.SetMessage(self.msg if self.msg else _("SVG ready for drag'n'drop"))
+        
+    def AnalyseWidgetAndUpdateUI(self, fname):
+        self.msg = ""
+        self.ResetSignature()
+
+        try:
+            if self.selected_SVG is None:
+                raise Exception(_("No widget selected"))
+
+            transform = XSLTransform(
+                os.path.join(ScriptDirectory, "analyse_widget.xslt"),[])
+
+            svgdom = etree.parse(self.selected_SVG)
+
+            signature = transform.transform(svgdom)
+
+            for entry in transform.get_error_log():
+                self.msg += "XSLT: " + entry.message + "\n" 
+
+        except Exception as e:
+            self.msg += str(e)
+            return
+        except XSLTApplyError as e:
+            self.msg += "Widget " + fname + " analysis error: " + e.message
+            return
+            
+        self.msg += "Widget " + fname + ": OK"
+
+        widgets = signature.getroot()
+        widget = widgets.find("widget")
+        defs = widget.find("defs")
+        # Keep double newlines (to mark paragraphs)
+        widget_desc = widget.find("desc")
+        self.desc.SetValue(
+            fname + ":\n\n" + (
+                _("No description given") if widget_desc is None else 
+                KeepDoubleNewLines(widget_desc.text)
+            ) + "\n\n" +
+            defs.find("type").text + " Widget: "+defs.find("shortdesc").text+"\n\n" +
+            KeepDoubleNewLines(defs.find("longdesc").text))
+        prefillargs = widget.findall("arg")
+        args = defs.findall("arg")
+        # extend args description in prefilled args in longer 
+        # (case of variable list of args)
+        if len(prefillargs) < len(args):
+            prefillargs += [None]*(len(args)-len(prefillargs))
+        if args and len(prefillargs) > len(args):
+            # TODO: check ordinality of last arg
+            # TODO: check that only last arg has multiple ordinality
+            args += [args[-1]]*(len(prefillargs)-len(args))
+        self.args_box.Show(len(args)!=0)
+        for arg, prefillarg in izip(args,prefillargs):
+            self.AddArgToSignature(arg, prefillarg)
+        paths = defs.findall("path")
+        self.paths_box.Show(len(paths)!=0)
+        for path in paths:
+            self.AddPathToSignature(path)
+
+        for widget in widgets:
+            widget_type = widget.get("type")
+            for path in widget.iterchildren("path"):
+                path_value = path.get("value")
+                path_accepts = map(
+                    str.strip, path.get("accepts", '')[1:-1].split(','))
+
+        self.main_panel.SetupScrolling(scroll_x=False)
+
+    def GetWidgetParams(self, _context):
+        args,paths = self.GenDnDSVGParams
+        root = etree.Element("params")
+        for arg in args:
+            etree.SubElement(root, "arg", value=arg)
+        for path in paths:
+            etree.SubElement(root, "path", value=path)
+        return root
+
+
+    def GenDnDSVG(self, newparams):
+        self.msg = ""
+
+        self.GenDnDSVGParams = newparams
+
+        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"))
+
+            transform = XSLTransform(
+                os.path.join(ScriptDirectory, "gen_dnd_widget_svg.xslt"),
+                [("GetWidgetParams", self.GetWidgetParams)])
+
+            svgdom = etree.parse(self.selected_SVG)
+
+            result = transform.transform(svgdom)
+
+            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):
+
+    def __init__(self, parent, register_for_HMI_tree_updates):
+        wx.SplitterWindow.__init__(self, parent,
+                                   style=wx.SUNKEN_BORDER | wx.SP_3D)
+
+        self.SelectionTree = HMITreeSelector(self)
+        self.Staging = WidgetLibBrowser(self)
+        self.SplitVertically(self.SelectionTree, self.Staging, 300)
+        register_for_HMI_tree_updates(weakref.ref(self))
+
+    def HMITreeUpdate(self, hmi_tree_root):
+        self.SelectionTree.MakeTree(hmi_tree_root)
+
+    def OnHMITreeNodeSelection(self, hmitree_nodes):
+        self.Staging.OnHMITreeNodeSelection(hmitree_nodes)