Edouard@2745: #!/usr/bin/env python
Edouard@2745: # -*- coding: utf-8 -*-
Edouard@2745: 
Edouard@2745: # This file is part of Beremiz
edouard@3197: # Copyright (C) 2021: Edouard TISSERANT
Edouard@2745: #
Edouard@2745: # See COPYING file for copyrights details.
Edouard@2745: 
Edouard@2745: from __future__ import absolute_import
Edouard@2745: import os
Edouard@2789: import hashlib
Edouard@2816: import weakref
edouard@3221: from tempfile import NamedTemporaryFile
Edouard@2745: 
Edouard@2745: import wx
Edouard@2816: 
edouard@3221: from lxml import etree
edouard@3221: from lxml.etree import XSLTApplyError
edouard@3221: from XSLTransform import XSLTransform
edouard@3221: 
edouard@3221: import util.paths as paths
edouard@3193: from IDEFrame import EncodeFileSystemPath, DecodeFileSystemPath
edouard@3201: from docutil import get_inkscape_path
Edouard@2745: 
Edouard@2756: from util.ProcessLogger import ProcessLogger
Edouard@2816: 
edouard@3221: ScriptDirectory = paths.AbsDir(__file__)
edouard@3221: 
Edouard@2818: class HMITreeSelector(wx.TreeCtrl):
Edouard@2818:     def __init__(self, parent):
Edouard@2818:         global on_hmitree_update
edouard@3177:         wx.TreeCtrl.__init__(self, parent, style=(
edouard@3177:             wx.TR_MULTIPLE |
edouard@3177:             wx.TR_HAS_BUTTONS |
edouard@3177:             wx.SUNKEN_BORDER |
edouard@3177:             wx.TR_LINES_AT_ROOT))
Edouard@2818: 
Edouard@2818:         self.MakeTree()
Edouard@2818: 
Edouard@2818:     def _recurseTree(self, current_hmitree_root, current_tc_root):
Edouard@2818:         for c in current_hmitree_root.children:
Edouard@2818:             if hasattr(c, "children"):
Edouard@2818:                 display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \
Edouard@2818:                                if c.hmiclass is not None else c.name
Edouard@2818:                 tc_child = self.AppendItem(current_tc_root, display_name)
edouard@3221:                 self.SetPyData(tc_child, c)
Edouard@2818: 
Edouard@2818:                 self._recurseTree(c,tc_child)
Edouard@2818:             else:
Edouard@2818:                 display_name = '{} {}'.format(c.nodetype[4:], c.name)
Edouard@2818:                 tc_child = self.AppendItem(current_tc_root, display_name)
edouard@3221:                 self.SetPyData(tc_child, c)
Edouard@2818: 
edouard@3201:     def MakeTree(self, hmi_tree_root=None):
Edouard@2818: 
Edouard@2818:         self.Freeze()
Edouard@2818: 
Edouard@2818:         self.root = None
Edouard@2818:         self.DeleteAllItems()
Edouard@2818: 
edouard@3193:         root_display_name = _("Please build to see HMI Tree") \
edouard@3193:             if hmi_tree_root is None else "HMI"
Edouard@2818:         self.root = self.AddRoot(root_display_name)
edouard@3221:         self.SetPyData(self.root, hmi_tree_root)
Edouard@2818: 
Edouard@2818:         if hmi_tree_root is not None:
Edouard@2818:             self._recurseTree(hmi_tree_root, self.root)
edouard@3177:             self.Expand(self.root)
Edouard@2818: 
Edouard@2818:         self.Thaw()
Edouard@2816: 
edouard@3193: class WidgetPicker(wx.TreeCtrl):
edouard@3193:     def __init__(self, parent, initialdir=None):
edouard@3193:         wx.TreeCtrl.__init__(self, parent, style=(
edouard@3193:             wx.TR_MULTIPLE |
edouard@3193:             wx.TR_HAS_BUTTONS |
edouard@3193:             wx.SUNKEN_BORDER |
edouard@3193:             wx.TR_LINES_AT_ROOT))
edouard@3193: 
edouard@3193:         self.MakeTree(initialdir)
edouard@3193: 
edouard@3193:     def _recurseTree(self, current_dir, current_tc_root, dirlist):
edouard@3193:         """
edouard@3193:         recurse through subdirectories, but creates tree nodes 
edouard@3193:         only when (sub)directory conbtains .svg file
edouard@3193:         """
edouard@3193:         res = []
edouard@3193:         for f in sorted(os.listdir(current_dir)):
edouard@3193:             p = os.path.join(current_dir,f)
edouard@3193:             if os.path.isdir(p):
edouard@3193: 
edouard@3193:                 r = self._recurseTree(p, current_tc_root, dirlist + [f])
edouard@3193:                 if len(r) > 0 :
edouard@3193:                     res = r
edouard@3193:                     dirlist = []
edouard@3193:                     current_tc_root = res.pop()
edouard@3193: 
edouard@3193:             elif os.path.splitext(f)[1].upper() == ".SVG":
edouard@3193:                 if len(dirlist) > 0 :
edouard@3193:                     res = []
edouard@3193:                     for d in dirlist:
edouard@3193:                         current_tc_root = self.AppendItem(current_tc_root, d)
edouard@3193:                         res.append(current_tc_root)
edouard@3193:                         self.SetPyData(current_tc_root, None)
edouard@3193:                     dirlist = []
edouard@3193:                     res.pop()
edouard@3193:                 tc_child = self.AppendItem(current_tc_root, f)
edouard@3193:                 self.SetPyData(tc_child, p)
edouard@3193:         return res
edouard@3193: 
edouard@3193:     def MakeTree(self, lib_dir = None):
edouard@3193: 
edouard@3193:         self.Freeze()
edouard@3193: 
edouard@3193:         self.root = None
edouard@3193:         self.DeleteAllItems()
edouard@3193: 
edouard@3193:         root_display_name = _("Please select widget library directory") \
edouard@3193:             if lib_dir is None else os.path.basename(lib_dir)
edouard@3193:         self.root = self.AddRoot(root_display_name)
edouard@3193:         self.SetPyData(self.root, None)
edouard@3193: 
edouard@3193:         if lib_dir is not None:
edouard@3193:             self._recurseTree(lib_dir, self.root, [])
edouard@3193:             self.Expand(self.root)
edouard@3193: 
edouard@3193:         self.Thaw()
edouard@3193: 
edouard@3193: _conf_key = "SVGHMIWidgetLib"
edouard@3193: _preview_height = 200
edouard@3241: _preview_margin = 5
edouard@3193: class WidgetLibBrowser(wx.Panel):
edouard@3193:     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
edouard@3193:                  size=wx.DefaultSize):
edouard@3193: 
edouard@3193:         wx.Panel.__init__(self, parent, id, pos, size)     
edouard@3193: 
edouard@3193:         self.bmp = None
edouard@3193:         self.msg = None
edouard@3193:         self.hmitree_node = None
edouard@3193:         self.selected_SVG = None
edouard@3193: 
edouard@3193:         self.Config = wx.ConfigBase.Get()
edouard@3193:         self.libdir = self.RecallLibDir()
edouard@3193: 
edouard@3241:         self.main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=5, vgap=0)
edouard@3241:         self.main_sizer.AddGrowableCol(0)
edouard@3241:         self.main_sizer.AddGrowableRow(1)
edouard@3193:         self.libbutton = wx.Button(self, -1, _("Select SVG widget library"))
edouard@3193:         self.widgetpicker = WidgetPicker(self, self.libdir)
edouard@3241:         self.preview = wx.Panel(self, size=(-1, _preview_height + _preview_margin*2))
edouard@3241:         self.desc = wx.TextCtrl(self, size=wx.Size(-1, 160),
edouard@3228:                                    style=wx.TE_READONLY | wx.TE_MULTILINE)
edouard@3237:         self.signature_sizer = wx.BoxSizer(wx.VERTICAL)
edouard@3241:         self.main_sizer.Add(self.libbutton, flag=wx.GROW)
edouard@3241:         self.main_sizer.Add(self.widgetpicker, flag=wx.GROW)
edouard@3241:         self.main_sizer.Add(self.preview, flag=wx.GROW)
edouard@3241:         self.main_sizer.Add(self.desc, flag=wx.GROW)
edouard@3241:         self.main_sizer.Add(self.signature_sizer, flag=wx.GROW)
edouard@3241:         self.main_sizer.Layout()
edouard@3193:         self.SetAutoLayout(True)
edouard@3241:         self.SetSizer(self.main_sizer)
edouard@3241:         self.main_sizer.Fit(self)
edouard@3193:         self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton)
edouard@3193:         self.preview.Bind(wx.EVT_PAINT, self.OnPaint)
edouard@3213:         self.preview.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
edouard@3193: 
edouard@3193:         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
edouard@3193: 
edouard@3193:         self.msg = _("Drag selected Widget from here to Inkscape")
edouard@3221:         self.tempf = None 
edouard@3193: 
edouard@3241:         self.paths_editors = []
edouard@3241: 
edouard@3241:     def ResetSignature(self):
edouard@3241:         self.signature_sizer.Clear()
edouard@3241:         for editor in self.paths_editors:
edouard@3241:             editor.Destroy()
edouard@3241:         self.paths_editors = []
edouard@3241:         self.main_sizer.Layout()
edouard@3241: 
edouard@3241:     def AddPathToSignature(self, path):
edouard@3241:         new_editor = wx.TextCtrl(self, size=wx.Size(-1, -1))
edouard@3241:         self.paths_editors.append(new_editor)
edouard@3241:         self.signature_sizer.Add(new_editor, flag=wx.GROW)
edouard@3241:         self.main_sizer.Layout()
edouard@3241: 
edouard@3193:     def RecallLibDir(self):
edouard@3193:         conf = self.Config.Read(_conf_key)
edouard@3193:         if len(conf) == 0:
edouard@3193:             return None
edouard@3193:         else:
edouard@3193:             return DecodeFileSystemPath(conf)
edouard@3193: 
edouard@3193:     def RememberLibDir(self, path):
edouard@3193:         self.Config.Write(_conf_key,
edouard@3193:                           EncodeFileSystemPath(path))
edouard@3193:         self.Config.Flush()
edouard@3193: 
edouard@3193:     def DrawPreview(self):
edouard@3193:         """
edouard@3193:         Refresh preview panel 
edouard@3193:         """
edouard@3193:         # Init preview panel paint device context
edouard@3193:         dc = wx.PaintDC(self.preview)
edouard@3193:         dc.Clear()
edouard@3193: 
edouard@3193:         if self.bmp:
edouard@3193:             # Get Preview panel size
edouard@3193:             sz = self.preview.GetClientSize()
edouard@3193:             w = self.bmp.GetWidth()
edouard@3241:             dc.DrawBitmap(self.bmp, (sz.width - w)/2, _preview_margin)
edouard@3193: 
edouard@3237:         self.desc.SetValue(self.msg)
edouard@3193: 
edouard@3193: 
edouard@3193:     def OnSelectLibDir(self, event):
edouard@3193:         defaultpath = self.RecallLibDir()
edouard@3193:         if defaultpath == None:
edouard@3193:             defaultpath = os.path.expanduser("~")
edouard@3193: 
edouard@3193:         dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath,
edouard@3193:                               style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
edouard@3193: 
edouard@3193:         if dialog.ShowModal() == wx.ID_OK:
edouard@3193:             self.libdir = dialog.GetPath()
edouard@3193:             self.RememberLibDir(self.libdir)
edouard@3193:             self.widgetpicker.MakeTree(self.libdir)
edouard@3193: 
edouard@3193:         dialog.Destroy()
edouard@3193: 
edouard@3193:     def OnPaint(self, event):
edouard@3193:         """
edouard@3193:         Called when Preview panel needs to be redrawn
edouard@3193:         @param event: wx.PaintEvent
edouard@3193:         """
edouard@3193:         self.DrawPreview()
edouard@3193:         event.Skip()
edouard@3193: 
edouard@3193:     def GenThumbnail(self, svgpath, thumbpath):
edouard@3193:         inkpath = get_inkscape_path()
edouard@3193:         if inkpath is None:
edouard@3193:             self.msg = _("Inkscape is not installed.")
edouard@3193:             return False
edouard@3193:         # TODO: spawn a thread, to decouple thumbnail gen
edouard@3193:         status, result, _err_result = ProcessLogger(
edouard@3193:             None,
edouard@3193:             '"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath +
edouard@3193:             '" -D -h ' + str(_preview_height)).spin()
edouard@3193:         if status != 0:
edouard@3193:             self.msg = _("Inkscape couldn't generate thumbnail.")
edouard@3193:             return False
edouard@3193:         return True
edouard@3193: 
edouard@3193:     def OnWidgetSelection(self, event):
edouard@3193:         """
edouard@3193:         Called when tree item is selected
edouard@3193:         @param event: wx.TreeEvent
edouard@3193:         """
edouard@3193:         item_pydata = self.widgetpicker.GetPyData(event.GetItem())
edouard@3193:         if item_pydata is not None:
edouard@3193:             svgpath = item_pydata
edouard@3193:             dname = os.path.dirname(svgpath)
edouard@3193:             fname = os.path.basename(svgpath)
edouard@3193:             hasher = hashlib.new('md5')
edouard@3193:             with open(svgpath, 'rb') as afile:
edouard@3193:                 while True:
edouard@3193:                     buf = afile.read(65536)
edouard@3193:                     if len(buf) > 0:
edouard@3193:                         hasher.update(buf)
edouard@3193:                     else:
edouard@3193:                         break
edouard@3193:             digest = hasher.hexdigest()
edouard@3193:             thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png"
edouard@3193:             thumbdir = os.path.join(dname, ".svghmithumbs") 
edouard@3193:             thumbpath = os.path.join(thumbdir, thumbfname) 
edouard@3193: 
edouard@3193:             have_thumb = os.path.exists(thumbpath)
edouard@3193: 
edouard@3221:             try:
edouard@3221:                 if not have_thumb:
edouard@3193:                     if not os.path.exists(thumbdir):
edouard@3193:                         os.mkdir(thumbdir)
edouard@3193:                     have_thumb = self.GenThumbnail(svgpath, thumbpath)
edouard@3193: 
edouard@3221:                 self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
edouard@3221: 
edouard@3221:                 self.selected_SVG = svgpath if have_thumb else None
edouard@3241: 
edouard@3241:                 self.AnalyseWidgetAndUpdateUI()
edouard@3241: 
edouard@3221:             except IOError:
edouard@3221:                 self.msg = _("Widget library must be writable")
edouard@3193: 
edouard@3193:             self.Refresh()
edouard@3193:         event.Skip()
edouard@3193: 
edouard@3231:     def OnHMITreeNodeSelection(self, hmitree_nodes):
edouard@3231:         self.hmitree_node = hmitree_nodes[0] if len(hmitree_nodes) else None
edouard@3193:         self.ValidateWidget()
edouard@3193:         self.Refresh()
edouard@3193: 
edouard@3213:     def OnLeftDown(self, evt):
edouard@3221:         if self.tempf is not None:
edouard@3221:             filename = self.tempf.name
edouard@3213:             data = wx.FileDataObject()
edouard@3213:             data.AddFile(filename)
edouard@3213:             dropSource = wx.DropSource(self)
edouard@3213:             dropSource.SetData(data)
edouard@3213:             dropSource.DoDragDrop(wx.Drag_AllowMove)
edouard@3213: 
edouard@3221:     def GiveDetails(self, _context, msgs):
edouard@3221:         for msg in msgs:
edouard@3222:             self.msg += msg.text + "\n"
edouard@3221:         
edouard@3234:     def PassMessage(self, _context, msgs):
edouard@3234:         for msg in msgs:
edouard@3234:             self.msg += msg.text + "\n"
edouard@3234: 
edouard@3222:     def GetSubHMITree(self, _context):
edouard@3222:         return [self.hmitree_node.etree()]
edouard@3241: 
edouard@3241:     def AnalyseWidgetAndUpdateUI(self):
edouard@3235:         self.msg = ""
edouard@3235: 
edouard@3235:         try:
edouard@3235:             if self.selected_SVG is None:
edouard@3235:                 raise Exception(_("No widget selected"))
edouard@3235: 
edouard@3235:             transform = XSLTransform(
edouard@3235:                 os.path.join(ScriptDirectory, "analyse_widget.xslt"),[])
edouard@3235: 
edouard@3235:             svgdom = etree.parse(self.selected_SVG)
edouard@3235: 
edouard@3241:             signature = transform.transform(svgdom)
edouard@3235: 
edouard@3235:             for entry in transform.get_error_log():
edouard@3235:                 self.msg += "XSLT: " + entry.message + "\n" 
edouard@3235: 
edouard@3235:         except Exception as e:
edouard@3235:             self.msg += str(e)
edouard@3235:         except XSLTApplyError as e:
edouard@3235:             self.msg += "Widget analysis error: " + e.message
edouard@3235:         else:
edouard@3241:             
edouard@3241:             self.ResetSignature()
edouard@3241: 
edouard@3235:             print(etree.tostring(signature, pretty_print=True))
edouard@3235:             widgets = signature.getroot()
edouard@3241:             for defs in widgets.iter("defs"):
edouard@3241: 
edouard@3241:                 # Keep double newlines (to mark paragraphs)
edouard@3241:                 self.msg += defs.find("type").text + ":\n" + "\n\n".join(map(
edouard@3241:                     lambda s:s.replace("\n"," ").replace("  ", " "), 
edouard@3241:                     defs.find("longdesc").text.split("\n\n")))
edouard@3241:                 for arg in defs.iter("arg"):
edouard@3241:                     print(arg.get("name"))
edouard@3241:                     print(arg.get("accepts"))
edouard@3241:                 for path in defs.iter("path"):
edouard@3241:                     self.AddPathToSignature(path)
edouard@3241:                     print(path.get("name"))
edouard@3241:                     print(path.get("accepts"))
edouard@3241: 
edouard@3235:             for widget in widgets:
edouard@3235:                 widget_type = widget.get("type")
edouard@3235:                 print(widget_type)
edouard@3241:                 for path in widget.iterchildren("path"):
edouard@3235:                     path_value = path.get("value")
edouard@3235:                     path_accepts = map(
edouard@3235:                         str.strip, path.get("accepts", '')[1:-1].split(','))
edouard@3241:                     print(path, path_value, path_accepts)
edouard@3235: 
edouard@3235: 
edouard@3222: 
edouard@3193:     def ValidateWidget(self):
edouard@3221:         self.msg = ""
edouard@3221: 
edouard@3221:         if self.tempf is not None:
edouard@3221:             os.unlink(self.tempf.name)
edouard@3221:             self.tempf = None
edouard@3221: 
edouard@3221:         try:
edouard@3221:             if self.selected_SVG is None:
edouard@3221:                 raise Exception(_("No widget selected"))
edouard@3221:             if self.hmitree_node is None:
edouard@3221:                 raise Exception(_("No HMI tree node selected"))
edouard@3221: 
edouard@3221:             transform = XSLTransform(
edouard@3221:                 os.path.join(ScriptDirectory, "gen_dnd_widget_svg.xslt"),
edouard@3222:                 [("GetSubHMITree", self.GetSubHMITree),
edouard@3234:                  ("PassMessage", self.GiveDetails)])
edouard@3221: 
edouard@3221:             svgdom = etree.parse(self.selected_SVG)
edouard@3221: 
edouard@3223:             result = transform.transform(
edouard@3223:                 svgdom, hmi_path = self.hmitree_node.hmi_path())
edouard@3221: 
edouard@3221:             for entry in transform.get_error_log():
edouard@3221:                 self.msg += "XSLT: " + entry.message + "\n" 
edouard@3221: 
edouard@3221:             self.tempf = NamedTemporaryFile(suffix='.svg', delete=False)
edouard@3221:             result.write(self.tempf, encoding="utf-8")
edouard@3221:             self.tempf.close()
edouard@3221: 
edouard@3221:         except Exception as e:
edouard@3221:             self.msg += str(e)
edouard@3221:         except XSLTApplyError as e:
edouard@3221:             self.msg += "Widget transform error: " + e.message
edouard@3221:                 
edouard@3221:     def __del__(self):
edouard@3221:         if self.tempf is not None:
edouard@3221:             os.unlink(self.tempf.name)
Edouard@2816: 
edouard@3201: class SVGHMI_UI(wx.SplitterWindow):
edouard@3201: 
edouard@3201:     def __init__(self, parent, register_for_HMI_tree_updates):
Edouard@2818:         wx.SplitterWindow.__init__(self, parent,
Edouard@2818:                                    style=wx.SUNKEN_BORDER | wx.SP_3D)
Edouard@2818: 
edouard@3231:         self.ordered_items = []
edouard@3231: 
Edouard@2818:         self.SelectionTree = HMITreeSelector(self)
edouard@3193:         self.Staging = WidgetLibBrowser(self)
edouard@3193:         self.SplitVertically(self.SelectionTree, self.Staging, 300)
edouard@3201:         register_for_HMI_tree_updates(weakref.ref(self))
edouard@3221:         self.Bind(wx.EVT_TREE_SEL_CHANGED,
edouard@3221:             self.OnHMITreeNodeSelection, self.SelectionTree)
edouard@3221: 
edouard@3221:     def OnHMITreeNodeSelection(self, event):
edouard@3231:         items = self.SelectionTree.GetSelections()
edouard@3231:         items_pydata = [self.SelectionTree.GetPyData(item) for item in items]
edouard@3231: 
edouard@3231:         # append new items to ordered item list
edouard@3231:         for item_pydata in items_pydata:
edouard@3231:             if item_pydata not in self.ordered_items:
edouard@3231:                 self.ordered_items.append(item_pydata)
edouard@3231: 
edouard@3231:         # filter out vanished items
edouard@3231:         self.ordered_items = [
edouard@3231:             item_pydata 
edouard@3231:             for item_pydata in self.ordered_items 
edouard@3231:             if item_pydata in items_pydata]
edouard@3231: 
edouard@3231:         self.Staging.OnHMITreeNodeSelection(items_pydata)
edouard@3201: 
edouard@3201:     def HMITreeUpdate(self, hmi_tree_root):
edouard@3201:             self.SelectionTree.MakeTree(hmi_tree_root)
edouard@3201: