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@3252: import re
edouard@3499: import tempfile
edouard@3261: from threading import Thread, Lock
edouard@3252: from functools import reduce
edouard@3259: from itertools import izip
edouard@3252: from operator import or_
edouard@3221: from tempfile import NamedTemporaryFile
Edouard@2745: 
Edouard@2745: import wx
Edouard@3245: from wx.lib.scrolledpanel import ScrolledPanel
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@3499: from docutil import get_inkscape_path, get_inkscape_version
Edouard@2745: 
Edouard@2756: from util.ProcessLogger import ProcessLogger
Edouard@2816: 
edouard@3221: ScriptDirectory = paths.AbsDir(__file__)
edouard@3221: 
edouard@3247: HMITreeDndMagicWord = "text/beremiz-hmitree"
edouard@3247: 
Edouard@2818: class HMITreeSelector(wx.TreeCtrl):
Edouard@2818:     def __init__(self, parent):
edouard@3247: 
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@3247:         self.ordered_items = []
edouard@3247:         self.parent = parent
edouard@3247: 
Edouard@2818:         self.MakeTree()
Edouard@2818: 
edouard@3247:         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeNodeSelection)
edouard@3247:         self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag)
edouard@3247: 
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@3247:     def OnTreeNodeSelection(self, event):
edouard@3247:         items = self.GetSelections()
edouard@3247:         items_pydata = [self.GetPyData(item) for item in items]
edouard@3247: 
edouard@3247:         # append new items to ordered item list
edouard@3247:         for item_pydata in items_pydata:
edouard@3247:             if item_pydata not in self.ordered_items:
edouard@3247:                 self.ordered_items.append(item_pydata)
edouard@3247: 
edouard@3247:         # filter out vanished items
edouard@3247:         self.ordered_items = [
edouard@3247:             item_pydata 
edouard@3247:             for item_pydata in self.ordered_items 
edouard@3247:             if item_pydata in items_pydata]
edouard@3247: 
edouard@3247:         self.parent.OnHMITreeNodeSelection(self.ordered_items)
edouard@3247: 
edouard@3247:     def OnTreeBeginDrag(self, event):
edouard@3247:         """
edouard@3247:         Called when a drag is started in tree
edouard@3247:         @param event: wx.TreeEvent
edouard@3247:         """
edouard@3247:         if self.ordered_items:
edouard@3247:             # Just send a recognizable mime-type, drop destination
edouard@3247:             # will get python data from parent
edouard@3247:             data = wx.CustomDataObject(HMITreeDndMagicWord)
edouard@3247:             dragSource = wx.DropSource(self)
edouard@3247:             dragSource.SetData(data)
edouard@3247:             dragSource.DoDragDrop()
edouard@3247: 
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@3253:         if lib_dir is not None and os.path.exists(lib_dir):
edouard@3193:             self._recurseTree(lib_dir, self.root, [])
edouard@3193:             self.Expand(self.root)
edouard@3193: 
edouard@3193:         self.Thaw()
edouard@3193: 
edouard@3247: class PathDropTarget(wx.DropTarget):
edouard@3247: 
edouard@3247:     def __init__(self, parent):
edouard@3247:         data = wx.CustomDataObject(HMITreeDndMagicWord)
edouard@3247:         wx.DropTarget.__init__(self, data)
edouard@3247:         self.ParentWindow = parent
edouard@3247: 
edouard@3247:     def OnDrop(self, x, y):
edouard@3247:         self.ParentWindow.OnHMITreeDnD()
edouard@3247:         return True
edouard@3247: 
edouard@3247: class ParamEditor(wx.Panel):
edouard@3247:     def __init__(self, parent, paramdesc):
edouard@3247:         wx.Panel.__init__(self, parent.main_panel)
edouard@3247:         label = paramdesc.get("name")+ ": " + paramdesc.get("accepts") 
edouard@3247:         if paramdesc.text:
edouard@3247:             label += "\n\"" + paramdesc.text + "\""
edouard@3243:         self.desc = wx.StaticText(self, label=label)
edouard@3247:         self.valid_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK, wx.ART_TOOLBAR, (16,16))
edouard@3247:         self.invalid_bmp = wx.ArtProvider.GetBitmap(wx.ART_CROSS_MARK, wx.ART_TOOLBAR, (16,16))
edouard@3243:         self.validity_sbmp = wx.StaticBitmap(self, -1, self.invalid_bmp)
edouard@3243:         self.edit = wx.TextCtrl(self)
edouard@3247:         self.edit_sizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0)
edouard@3247:         self.edit_sizer.AddGrowableCol(0)
edouard@3243:         self.edit_sizer.AddGrowableRow(0)
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@3261:     def GetValue(self):
edouard@3261:         return self.edit.GetValue()
edouard@3261: 
edouard@3252:     def setValidity(self, validity):
edouard@3252:         if validity is not None:
edouard@3252:             bmp = self.valid_bmp if validity else self.invalid_bmp
edouard@3252:             self.validity_sbmp.SetBitmap(bmp)
edouard@3252:             self.validity_sbmp.Show(True)
edouard@3252:         else :
edouard@3252:             self.validity_sbmp.Show(False)
edouard@3252: 
edouard@3252: models = { typename: re.compile(regex) for typename, regex in [
edouard@3252:     ("string", r".*"),
Edouard@3263:     ("int", r"^-?([1-9][0-9]*|0)$"),
Edouard@3263:     ("real", r"^-?([1-9][0-9]*|0)(\.[0-9]+)?$")]}
edouard@3251: 
edouard@3247: class ArgEditor(ParamEditor):
edouard@3259:     def __init__(self, parent, argdesc, prefillargdesc):
edouard@3252:         ParamEditor.__init__(self, parent, argdesc)
edouard@3252:         self.ParentObj = parent
edouard@3252:         self.argdesc = argdesc
edouard@3252:         self.Bind(wx.EVT_TEXT, self.OnArgChanged, self.edit)
edouard@3259:         prefill = "" if prefillargdesc is None else prefillargdesc.get("value")
edouard@3259:         self.edit.SetValue(prefill)
edouard@3259:         # TODO add a button to add more ArgEditror instance 
edouard@3259:         #      when ordinality is multiple
edouard@3252: 
edouard@3252:     def OnArgChanged(self, event):
edouard@3252:         txt = self.edit.GetValue()
edouard@3252:         accepts = self.argdesc.get("accepts").split(',')
edouard@3252:         self.setValidity(
edouard@3252:             reduce(or_,
edouard@3252:                    map(lambda typename: 
edouard@3252:                            models[typename].match(txt) is not None,
edouard@3252:                        accepts), 
edouard@3252:                    False)
edouard@3252:             if accepts and txt else None)
edouard@3261:         self.ParentObj.RegenSVGLater()
edouard@3252:         event.Skip()
edouard@3247: 
edouard@3247: class PathEditor(ParamEditor):
edouard@3247:     def __init__(self, parent, pathdesc):
edouard@3247:         ParamEditor.__init__(self, parent, pathdesc)
edouard@3247:         self.ParentObj = parent
edouard@3251:         self.pathdesc = pathdesc
edouard@3247:         DropTarget = PathDropTarget(self)
edouard@3247:         self.edit.SetDropTarget(DropTarget)
Edouard@3265:         self.edit.SetHint(_("Drag'n'drop HMI variable here"))
edouard@3251:         self.Bind(wx.EVT_TEXT, self.OnPathChanged, self.edit)
edouard@3247: 
edouard@3247:     def OnHMITreeDnD(self):
edouard@3247:         self.ParentObj.GotPathDnDOn(self)
edouard@3247: 
edouard@3251:     def SetPath(self, hmitree_node):
edouard@3251:         self.edit.ChangeValue(hmitree_node.hmi_path())
edouard@3252:         self.setValidity(
edouard@3252:             hmitree_node.nodetype in self.pathdesc.get("accepts").split(","))
edouard@3247: 
edouard@3247:     def OnPathChanged(self, event):
edouard@3251:         # TODO : find corresponding hmitre node and type to update validity
edouard@3251:         # Lazy way : hide validity
edouard@3252:         self.setValidity(None)
edouard@3261:         self.ParentObj.RegenSVGLater()
edouard@3247:         event.Skip()
edouard@3247:     
edouard@3259: def KeepDoubleNewLines(txt):
edouard@3259:     return "\n\n".join(map(
edouard@3259:         lambda s:re.sub(r'\s+',' ',s),
edouard@3259:         txt.split("\n\n")))
edouard@3247: 
edouard@3193: _conf_key = "SVGHMIWidgetLib"
edouard@3193: _preview_height = 200
edouard@3241: _preview_margin = 5
edouard@3499: thumbnail_temp_path = None
edouard@3499: 
edouard@3243: class WidgetLibBrowser(wx.SplitterWindow):
edouard@3499:     def __init__(self, parent, controler, 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@3247:         self.hmitree_nodes = []
edouard@3193:         self.selected_SVG = None
edouard@3499:         self.Controler = controler
edouard@3193: 
edouard@3193:         self.Config = wx.ConfigBase.Get()
edouard@3193:         self.libdir = self.RecallLibDir()
Edouard@3274:         if self.libdir is None:
Edouard@3274:             self.libdir = os.path.join(ScriptDirectory, "widgetlib") 
edouard@3193: 
Edouard@3245:         self.picker_desc_splitter = wx.SplitterWindow(self, style=wx.SUNKEN_BORDER | wx.SP_3D)
Edouard@3245: 
Edouard@3245:         self.picker_panel = wx.Panel(self.picker_desc_splitter)
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@3245:         self.main_panel = ScrolledPanel(parent=self,
Edouard@3245:                                                 name='MiscellaneousPanel',
Edouard@3245:                                                 style=wx.TAB_TRAVERSAL)
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@3247:         self.staticmsg = wx.StaticText(self, label = _("Drag selected Widget from here to Inkscape"))
edouard@3243:         self.preview = wx.Panel(self.main_panel, size=(-1, _preview_height + _preview_margin*2))
edouard@3237:         self.signature_sizer = wx.BoxSizer(wx.VERTICAL)
edouard@3247:         self.args_box = wx.StaticBox(self.main_panel, -1,
edouard@3247:                                      _("Widget's arguments"),
edouard@3259:                                      style = wx.ALIGN_CENTRE_HORIZONTAL)
edouard@3247:         self.args_sizer = wx.StaticBoxSizer(self.args_box, wx.VERTICAL)
edouard@3247:         self.paths_box = wx.StaticBox(self.main_panel, -1,
edouard@3247:                                       _("Widget's variables"),
edouard@3259:                                       style = wx.ALIGN_CENTRE_HORIZONTAL)
edouard@3247:         self.paths_sizer = wx.StaticBoxSizer(self.paths_box, wx.VERTICAL)
edouard@3247:         self.signature_sizer.Add(self.args_sizer, flag=wx.GROW)
edouard@3247:         self.signature_sizer.AddSpacer(5)
edouard@3247:         self.signature_sizer.Add(self.paths_sizer, flag=wx.GROW)
edouard@3247:         self.main_sizer.Add(self.staticmsg, flag=wx.GROW)
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.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@3245:         self.desc = wx.TextCtrl(self.picker_desc_splitter, size=wx.Size(-1, 160),
Edouard@3245:                                    style=wx.TE_READONLY | wx.TE_MULTILINE)
Edouard@3245: 
Edouard@3245:         self.picker_desc_splitter.SplitHorizontally(self.picker_panel, self.desc, 400)
Edouard@3245:         self.SplitVertically(self.main_panel, self.picker_desc_splitter, 300)
edouard@3193: 
edouard@3221:         self.tempf = None 
edouard@3193: 
edouard@3261:         self.RegenSVGThread = None
edouard@3261:         self.RegenSVGLock = Lock()
edouard@3261:         self.RegenSVGTimer = wx.Timer(self, -1)
edouard@3261:         self.RegenSVGParams = None
edouard@3261:         self.Bind(wx.EVT_TIMER,
edouard@3261:                   self.RegenSVG,
edouard@3261:                   self.RegenSVGTimer)
edouard@3261: 
edouard@3247:         self.args_editors = []
edouard@3241:         self.paths_editors = []
edouard@3241: 
Edouard@3263:     def SetMessage(self, msg):
Edouard@3263:         self.staticmsg.SetLabel(msg)
Edouard@3263:         self.main_sizer.Layout()
Edouard@3263: 
edouard@3241:     def ResetSignature(self):
edouard@3247:         self.args_sizer.Clear()
edouard@3247:         for editor in self.args_editors:
edouard@3247:             editor.Destroy()
edouard@3247:         self.args_editors = []
edouard@3247: 
edouard@3247:         self.paths_sizer.Clear()
edouard@3241:         for editor in self.paths_editors:
edouard@3241:             editor.Destroy()
edouard@3241:         self.paths_editors = []
edouard@3241: 
edouard@3259:     def AddArgToSignature(self, arg, prefillarg):
edouard@3259:         new_editor = ArgEditor(self, arg, prefillarg)
edouard@3247:         self.args_editors.append(new_editor)
edouard@3247:         self.args_sizer.Add(new_editor, flag=wx.GROW)
edouard@3247: 
edouard@3241:     def AddPathToSignature(self, path):
edouard@3247:         new_editor = PathEditor(self, path)
edouard@3241:         self.paths_editors.append(new_editor)
edouard@3247:         self.paths_sizer.Add(new_editor, flag=wx.GROW)
edouard@3247: 
edouard@3247:     def GotPathDnDOn(self, target_editor):
edouard@3247:         dndindex = self.paths_editors.index(target_editor)
edouard@3247: 
edouard@3251:         for hmitree_node,editor in zip(self.hmitree_nodes,
edouard@3247:                                    self.paths_editors[dndindex:]):
edouard@3251:             editor.SetPath(hmitree_node)
edouard@3241: 
edouard@3261:         self.RegenSVGNow()
edouard@3261: 
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@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@3499: 
edouard@3499:         export_opt = "-o" if get_inkscape_version()[0] > 0 else "-e"
edouard@3499: 
edouard@3193:         # TODO: spawn a thread, to decouple thumbnail gen
edouard@3193:         status, result, _err_result = ProcessLogger(
edouard@3499:             self.Controler.GetCTRoot().logger,
edouard@3499:             '"' + inkpath + '" "' + svgpath + '" ' +
edouard@3499:             export_opt + ' "' + 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@3499:         global thumbnail_temp_path
edouard@3499:         event.Skip()
edouard@3193:         item_pydata = self.widgetpicker.GetPyData(event.GetItem())
edouard@3193:         if item_pydata is not None:
edouard@3193:             svgpath = item_pydata
edouard@3499: 
edouard@3499:             if thumbnail_temp_path is None:
edouard@3499:                 try:
edouard@3499:                     dname = os.path.dirname(svgpath)
edouard@3499:                     thumbdir = os.path.join(dname, ".svghmithumbs") 
edouard@3499:                     if not os.path.exists(thumbdir):
edouard@3499:                         os.mkdir(thumbdir)
edouard@3499:                 except Exception :
edouard@3499:                     # library not writable : use temp dir
edouard@3499:                     thumbnail_temp_path = os.path.join(
edouard@3499:                         tempfile.gettempdir(), "svghmithumbs")
edouard@3499:                     thumbdir = thumbnail_temp_path
edouard@3499:                     if not os.path.exists(thumbdir):
edouard@3499:                         os.mkdir(thumbdir)
edouard@3499:             else:
edouard@3499:                 thumbdir = thumbnail_temp_path
edouard@3499: 
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:             thumbpath = os.path.join(thumbdir, thumbfname) 
edouard@3193: 
edouard@3193:             have_thumb = os.path.exists(thumbpath)
edouard@3193: 
edouard@3499:             if not have_thumb:
edouard@3499:                 self.Controler.GetCTRoot().logger.write(
edouard@3499:                     "Rendering preview of " + fname + " widget.\n")
edouard@3499:                 have_thumb = self.GenThumbnail(svgpath, thumbpath)
edouard@3499: 
edouard@3499:             self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
edouard@3499: 
edouard@3499:             self.selected_SVG = svgpath if have_thumb else None
edouard@3499: 
edouard@3499:             self.AnalyseWidgetAndUpdateUI(fname)
edouard@3499: 
edouard@3499:             self.SetMessage(self.msg)
edouard@3193: 
edouard@3193:             self.Refresh()
edouard@3193: 
edouard@3231:     def OnHMITreeNodeSelection(self, hmitree_nodes):
edouard@3247:         self.hmitree_nodes = hmitree_nodes
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@3261:     def RegenSVGLater(self, when=1):
Edouard@3263:         self.SetMessage(_("SVG generation pending"))
edouard@3261:         self.RegenSVGTimer.Start(milliseconds=when*1000, oneShot=True)
edouard@3261: 
edouard@3261:     def RegenSVGNow(self):
edouard@3261:         self.RegenSVGLater(when=0)
edouard@3261: 
edouard@3261:     def RegenSVG(self, event):
Edouard@3263:         self.SetMessage(_("Generating SVG..."))
edouard@3261:         args = [arged.GetValue() for arged in self.args_editors]
Edouard@3263:         while args and not args[-1]: args.pop(-1)
edouard@3261:         paths = [pathed.GetValue() for pathed in self.paths_editors]
Edouard@3263:         while paths and not paths[-1]: paths.pop(-1)
edouard@3261:         if self.RegenSVGLock.acquire(True):
edouard@3261:             self.RegenSVGParams = (args, paths)
edouard@3261:             if self.RegenSVGThread is None:
edouard@3261:                 self.RegenSVGThread = \
edouard@3261:                     Thread(target=self.RegenSVGProc,
edouard@3261:                            name="RegenSVGThread").start()
edouard@3261:             self.RegenSVGLock.release()
edouard@3261:         event.Skip()
edouard@3261: 
edouard@3261:     def RegenSVGProc(self):
edouard@3261:         self.RegenSVGLock.acquire(True)
edouard@3261: 
edouard@3261:         newparams = self.RegenSVGParams
edouard@3261:         self.RegenSVGParams = None
edouard@3261: 
edouard@3261:         while newparams is not None:
edouard@3261:             self.RegenSVGLock.release()
edouard@3261: 
edouard@3261:             res = self.GenDnDSVG(newparams)
edouard@3261: 
edouard@3261:             self.RegenSVGLock.acquire(True)
edouard@3261: 
edouard@3261:             newparams = self.RegenSVGParams
edouard@3261:             self.RegenSVGParams = None
edouard@3261: 
edouard@3261:         self.RegenSVGThread = None
edouard@3261: 
edouard@3261:         self.RegenSVGLock.release()
edouard@3261: 
edouard@3261:         wx.CallAfter(self.DoneRegenSVG)
edouard@3221:         
edouard@3261:     def DoneRegenSVG(self):
Edouard@3263:         self.SetMessage(self.msg if self.msg else _("SVG ready for drag'n'drop"))
edouard@3261:         
edouard@3247:     def AnalyseWidgetAndUpdateUI(self, fname):
edouard@3235:         self.msg = ""
Edouard@3245:         self.ResetSignature()
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@3259:             return
edouard@3235:         except XSLTApplyError as e:
edouard@3247:             self.msg += "Widget " + fname + " analysis error: " + e.message
edouard@3259:             return
edouard@3241:             
edouard@3259:         self.msg += "Widget " + fname + ": OK"
edouard@3259: 
edouard@3259:         widgets = signature.getroot()
edouard@3259:         widget = widgets.find("widget")
edouard@3259:         defs = widget.find("defs")
edouard@3259:         # Keep double newlines (to mark paragraphs)
edouard@3259:         widget_desc = widget.find("desc")
edouard@3259:         self.desc.SetValue(
Edouard@3279:             fname + ":\n\n" + (
edouard@3259:                 _("No description given") if widget_desc is None else 
edouard@3259:                 KeepDoubleNewLines(widget_desc.text)
edouard@3259:             ) + "\n\n" +
Edouard@3279:             defs.find("type").text + " Widget: "+defs.find("shortdesc").text+"\n\n" +
edouard@3259:             KeepDoubleNewLines(defs.find("longdesc").text))
edouard@3259:         prefillargs = widget.findall("arg")
edouard@3259:         args = defs.findall("arg")
edouard@3259:         # extend args description in prefilled args in longer 
edouard@3259:         # (case of variable list of args)
edouard@3259:         if len(prefillargs) < len(args):
edouard@3259:             prefillargs += [None]*(len(args)-len(prefillargs))
edouard@3259:         if args and len(prefillargs) > len(args):
edouard@3259:             # TODO: check ordinality of last arg
edouard@3259:             # TODO: check that only last arg has multiple ordinality
edouard@3259:             args += [args[-1]]*(len(prefillargs)-len(args))
edouard@3259:         self.args_box.Show(len(args)!=0)
edouard@3259:         for arg, prefillarg in izip(args,prefillargs):
edouard@3259:             self.AddArgToSignature(arg, prefillarg)
Edouard@3454: 
Edouard@3454:         # TODO support predefined path count (as for XYGraph)
edouard@3259:         paths = defs.findall("path")
edouard@3259:         self.paths_box.Show(len(paths)!=0)
edouard@3259:         for path in paths:
edouard@3259:             self.AddPathToSignature(path)
edouard@3259: 
Edouard@3245:         self.main_panel.SetupScrolling(scroll_x=False)
Edouard@3245: 
edouard@3261:     def GetWidgetParams(self, _context):
edouard@3261:         args,paths = self.GenDnDSVGParams
edouard@3261:         root = etree.Element("params")
edouard@3261:         for arg in args:
edouard@3261:             etree.SubElement(root, "arg", value=arg)
edouard@3261:         for path in paths:
edouard@3261:             etree.SubElement(root, "path", value=path)
edouard@3261:         return root
edouard@3261: 
edouard@3261: 
edouard@3261:     def GenDnDSVG(self, newparams):
edouard@3221:         self.msg = ""
edouard@3221: 
edouard@3261:         self.GenDnDSVGParams = newparams
edouard@3261: 
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: 
edouard@3221:             transform = XSLTransform(
edouard@3221:                 os.path.join(ScriptDirectory, "gen_dnd_widget_svg.xslt"),
Edouard@3263:                 [("GetWidgetParams", self.GetWidgetParams)])
edouard@3221: 
edouard@3221:             svgdom = etree.parse(self.selected_SVG)
edouard@3221: 
edouard@3261:             result = transform.transform(svgdom)
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@3499:     def __init__(self, parent, controler, register_for_HMI_tree_updates):
Edouard@2818:         wx.SplitterWindow.__init__(self, parent,
Edouard@2818:                                    style=wx.SUNKEN_BORDER | wx.SP_3D)
Edouard@2818: 
Edouard@2818:         self.SelectionTree = HMITreeSelector(self)
edouard@3499:         self.Staging = WidgetLibBrowser(self, controler)
edouard@3193:         self.SplitVertically(self.SelectionTree, self.Staging, 300)
edouard@3201:         register_for_HMI_tree_updates(weakref.ref(self))
edouard@3201: 
edouard@3201:     def HMITreeUpdate(self, hmi_tree_root):
edouard@3247:         self.SelectionTree.MakeTree(hmi_tree_root)
edouard@3247: 
edouard@3247:     def OnHMITreeNodeSelection(self, hmitree_nodes):
edouard@3247:         self.Staging.OnHMITreeNodeSelection(hmitree_nodes)