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@3243: class PathEditor(wx.Panel): edouard@3243: def __init__(self, parent, path): edouard@3243: edouard@3243: wx.Panel.__init__(self, parent) edouard@3243: label = path.get("name") + ": " + path.text + "(" + path.get("accepts") + ")" edouard@3243: self.desc = wx.StaticText(self, label=label) edouard@3243: self.focus_sbmp = wx.StaticBitmap(self, -1, wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_TOOLBAR, (32,32))) edouard@3243: self.valid_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK, wx.ART_TOOLBAR, (32,32)) edouard@3243: self.invalid_bmp = wx.ArtProvider.GetBitmap(wx.ART_CROSS_MARK, wx.ART_TOOLBAR, (32,32)) edouard@3243: self.validity_sbmp = wx.StaticBitmap(self, -1, self.invalid_bmp) edouard@3243: self.edit = wx.TextCtrl(self) edouard@3243: self.edit_sizer = wx.FlexGridSizer(cols=3, hgap=0, rows=1, vgap=0) edouard@3243: self.edit_sizer.AddGrowableCol(1) edouard@3243: self.edit_sizer.AddGrowableRow(0) edouard@3243: self.edit_sizer.Add(self.focus_sbmp, flag=wx.GROW) edouard@3243: self.edit_sizer.Add(self.edit, flag=wx.GROW) edouard@3243: self.edit_sizer.Add(self.validity_sbmp, flag=wx.GROW) edouard@3243: self.main_sizer = wx.BoxSizer(wx.VERTICAL) edouard@3243: self.main_sizer.Add(self.desc, flag=wx.GROW) edouard@3243: self.main_sizer.Add(self.edit_sizer, flag=wx.GROW) edouard@3243: self.SetSizer(self.main_sizer) edouard@3243: self.main_sizer.Fit(self) edouard@3243: edouard@3193: _conf_key = "SVGHMIWidgetLib" edouard@3193: _preview_height = 200 edouard@3241: _preview_margin = 5 edouard@3243: class WidgetLibBrowser(wx.SplitterWindow): edouard@3193: def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, edouard@3193: size=wx.DefaultSize): edouard@3193: edouard@3243: wx.SplitterWindow.__init__(self, parent, edouard@3243: style=wx.SUNKEN_BORDER | wx.SP_3D) 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@3243: self.picker_panel = wx.Panel(self) edouard@3243: self.picker_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) edouard@3243: self.picker_sizer.AddGrowableCol(0) edouard@3243: self.picker_sizer.AddGrowableRow(1) edouard@3243: edouard@3243: self.widgetpicker = WidgetPicker(self.picker_panel, self.libdir) edouard@3243: self.libbutton = wx.Button(self.picker_panel, -1, _("Select SVG widget library")) edouard@3243: edouard@3243: self.picker_sizer.Add(self.libbutton, flag=wx.GROW) edouard@3243: self.picker_sizer.Add(self.widgetpicker, flag=wx.GROW) edouard@3243: self.picker_sizer.Layout() edouard@3243: self.picker_panel.SetAutoLayout(True) edouard@3243: self.picker_panel.SetSizer(self.picker_sizer) edouard@3243: edouard@3243: self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker) edouard@3243: self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton) edouard@3243: edouard@3243: edouard@3243: edouard@3243: self.main_panel = wx.Panel(self) edouard@3243: edouard@3243: self.main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0) edouard@3241: self.main_sizer.AddGrowableCol(0) edouard@3243: self.main_sizer.AddGrowableRow(2) edouard@3243: edouard@3243: self.preview = wx.Panel(self.main_panel, size=(-1, _preview_height + _preview_margin*2)) edouard@3243: self.desc = wx.TextCtrl(self.main_panel, 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.preview, flag=wx.GROW) edouard@3243: self.main_sizer.Add(self.signature_sizer, flag=wx.GROW) edouard@3241: self.main_sizer.Add(self.desc, flag=wx.GROW) edouard@3241: self.main_sizer.Layout() edouard@3243: self.main_panel.SetAutoLayout(True) edouard@3243: self.main_panel.SetSizer(self.main_sizer) edouard@3243: self.main_sizer.Fit(self.main_panel) edouard@3193: self.preview.Bind(wx.EVT_PAINT, self.OnPaint) edouard@3213: self.preview.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) edouard@3193: Edouard@3244: self.SplitVertically(self.main_panel, self.picker_panel, 300) 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@3243: new_editor = PathEditor(self.main_panel, path) 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: