svghmi/ui.py
changeset 3302 c89fc366bebd
parent 3279 5615e062a77d
child 3303 0ffb41625592
child 3454 0b5ab53007a9
equal deleted inserted replaced
2744:577118ebd179 3302:c89fc366bebd
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 
       
     4 # This file is part of Beremiz
       
     5 # Copyright (C) 2021: Edouard TISSERANT
       
     6 #
       
     7 # See COPYING file for copyrights details.
       
     8 
       
     9 from __future__ import absolute_import
       
    10 import os
       
    11 import hashlib
       
    12 import weakref
       
    13 import re
       
    14 from threading import Thread, Lock
       
    15 from functools import reduce
       
    16 from itertools import izip
       
    17 from operator import or_
       
    18 from tempfile import NamedTemporaryFile
       
    19 
       
    20 import wx
       
    21 from wx.lib.scrolledpanel import ScrolledPanel
       
    22 
       
    23 from lxml import etree
       
    24 from lxml.etree import XSLTApplyError
       
    25 from XSLTransform import XSLTransform
       
    26 
       
    27 import util.paths as paths
       
    28 from IDEFrame import EncodeFileSystemPath, DecodeFileSystemPath
       
    29 from docutil import get_inkscape_path
       
    30 
       
    31 from util.ProcessLogger import ProcessLogger
       
    32 
       
    33 ScriptDirectory = paths.AbsDir(__file__)
       
    34 
       
    35 HMITreeDndMagicWord = "text/beremiz-hmitree"
       
    36 
       
    37 class HMITreeSelector(wx.TreeCtrl):
       
    38     def __init__(self, parent):
       
    39 
       
    40         wx.TreeCtrl.__init__(self, parent, style=(
       
    41             wx.TR_MULTIPLE |
       
    42             wx.TR_HAS_BUTTONS |
       
    43             wx.SUNKEN_BORDER |
       
    44             wx.TR_LINES_AT_ROOT))
       
    45 
       
    46         self.ordered_items = []
       
    47         self.parent = parent
       
    48 
       
    49         self.MakeTree()
       
    50 
       
    51         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeNodeSelection)
       
    52         self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag)
       
    53 
       
    54     def _recurseTree(self, current_hmitree_root, current_tc_root):
       
    55         for c in current_hmitree_root.children:
       
    56             if hasattr(c, "children"):
       
    57                 display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \
       
    58                                if c.hmiclass is not None else c.name
       
    59                 tc_child = self.AppendItem(current_tc_root, display_name)
       
    60                 self.SetPyData(tc_child, c)
       
    61 
       
    62                 self._recurseTree(c,tc_child)
       
    63             else:
       
    64                 display_name = '{} {}'.format(c.nodetype[4:], c.name)
       
    65                 tc_child = self.AppendItem(current_tc_root, display_name)
       
    66                 self.SetPyData(tc_child, c)
       
    67 
       
    68     def OnTreeNodeSelection(self, event):
       
    69         items = self.GetSelections()
       
    70         items_pydata = [self.GetPyData(item) for item in items]
       
    71 
       
    72         # append new items to ordered item list
       
    73         for item_pydata in items_pydata:
       
    74             if item_pydata not in self.ordered_items:
       
    75                 self.ordered_items.append(item_pydata)
       
    76 
       
    77         # filter out vanished items
       
    78         self.ordered_items = [
       
    79             item_pydata 
       
    80             for item_pydata in self.ordered_items 
       
    81             if item_pydata in items_pydata]
       
    82 
       
    83         self.parent.OnHMITreeNodeSelection(self.ordered_items)
       
    84 
       
    85     def OnTreeBeginDrag(self, event):
       
    86         """
       
    87         Called when a drag is started in tree
       
    88         @param event: wx.TreeEvent
       
    89         """
       
    90         if self.ordered_items:
       
    91             # Just send a recognizable mime-type, drop destination
       
    92             # will get python data from parent
       
    93             data = wx.CustomDataObject(HMITreeDndMagicWord)
       
    94             dragSource = wx.DropSource(self)
       
    95             dragSource.SetData(data)
       
    96             dragSource.DoDragDrop()
       
    97 
       
    98     def MakeTree(self, hmi_tree_root=None):
       
    99 
       
   100         self.Freeze()
       
   101 
       
   102         self.root = None
       
   103         self.DeleteAllItems()
       
   104 
       
   105         root_display_name = _("Please build to see HMI Tree") \
       
   106             if hmi_tree_root is None else "HMI"
       
   107         self.root = self.AddRoot(root_display_name)
       
   108         self.SetPyData(self.root, hmi_tree_root)
       
   109 
       
   110         if hmi_tree_root is not None:
       
   111             self._recurseTree(hmi_tree_root, self.root)
       
   112             self.Expand(self.root)
       
   113 
       
   114         self.Thaw()
       
   115 
       
   116 class WidgetPicker(wx.TreeCtrl):
       
   117     def __init__(self, parent, initialdir=None):
       
   118         wx.TreeCtrl.__init__(self, parent, style=(
       
   119             wx.TR_MULTIPLE |
       
   120             wx.TR_HAS_BUTTONS |
       
   121             wx.SUNKEN_BORDER |
       
   122             wx.TR_LINES_AT_ROOT))
       
   123 
       
   124         self.MakeTree(initialdir)
       
   125 
       
   126     def _recurseTree(self, current_dir, current_tc_root, dirlist):
       
   127         """
       
   128         recurse through subdirectories, but creates tree nodes 
       
   129         only when (sub)directory conbtains .svg file
       
   130         """
       
   131         res = []
       
   132         for f in sorted(os.listdir(current_dir)):
       
   133             p = os.path.join(current_dir,f)
       
   134             if os.path.isdir(p):
       
   135 
       
   136                 r = self._recurseTree(p, current_tc_root, dirlist + [f])
       
   137                 if len(r) > 0 :
       
   138                     res = r
       
   139                     dirlist = []
       
   140                     current_tc_root = res.pop()
       
   141 
       
   142             elif os.path.splitext(f)[1].upper() == ".SVG":
       
   143                 if len(dirlist) > 0 :
       
   144                     res = []
       
   145                     for d in dirlist:
       
   146                         current_tc_root = self.AppendItem(current_tc_root, d)
       
   147                         res.append(current_tc_root)
       
   148                         self.SetPyData(current_tc_root, None)
       
   149                     dirlist = []
       
   150                     res.pop()
       
   151                 tc_child = self.AppendItem(current_tc_root, f)
       
   152                 self.SetPyData(tc_child, p)
       
   153         return res
       
   154 
       
   155     def MakeTree(self, lib_dir = None):
       
   156 
       
   157         self.Freeze()
       
   158 
       
   159         self.root = None
       
   160         self.DeleteAllItems()
       
   161 
       
   162         root_display_name = _("Please select widget library directory") \
       
   163             if lib_dir is None else os.path.basename(lib_dir)
       
   164         self.root = self.AddRoot(root_display_name)
       
   165         self.SetPyData(self.root, None)
       
   166 
       
   167         if lib_dir is not None and os.path.exists(lib_dir):
       
   168             self._recurseTree(lib_dir, self.root, [])
       
   169             self.Expand(self.root)
       
   170 
       
   171         self.Thaw()
       
   172 
       
   173 class PathDropTarget(wx.DropTarget):
       
   174 
       
   175     def __init__(self, parent):
       
   176         data = wx.CustomDataObject(HMITreeDndMagicWord)
       
   177         wx.DropTarget.__init__(self, data)
       
   178         self.ParentWindow = parent
       
   179 
       
   180     def OnDrop(self, x, y):
       
   181         self.ParentWindow.OnHMITreeDnD()
       
   182         return True
       
   183 
       
   184 class ParamEditor(wx.Panel):
       
   185     def __init__(self, parent, paramdesc):
       
   186         wx.Panel.__init__(self, parent.main_panel)
       
   187         label = paramdesc.get("name")+ ": " + paramdesc.get("accepts") 
       
   188         if paramdesc.text:
       
   189             label += "\n\"" + paramdesc.text + "\""
       
   190         self.desc = wx.StaticText(self, label=label)
       
   191         self.valid_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK, wx.ART_TOOLBAR, (16,16))
       
   192         self.invalid_bmp = wx.ArtProvider.GetBitmap(wx.ART_CROSS_MARK, wx.ART_TOOLBAR, (16,16))
       
   193         self.validity_sbmp = wx.StaticBitmap(self, -1, self.invalid_bmp)
       
   194         self.edit = wx.TextCtrl(self)
       
   195         self.edit_sizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0)
       
   196         self.edit_sizer.AddGrowableCol(0)
       
   197         self.edit_sizer.AddGrowableRow(0)
       
   198         self.edit_sizer.Add(self.edit, flag=wx.GROW)
       
   199         self.edit_sizer.Add(self.validity_sbmp, flag=wx.GROW)
       
   200         self.main_sizer = wx.BoxSizer(wx.VERTICAL)
       
   201         self.main_sizer.Add(self.desc, flag=wx.GROW)
       
   202         self.main_sizer.Add(self.edit_sizer, flag=wx.GROW)
       
   203         self.SetSizer(self.main_sizer)
       
   204         self.main_sizer.Fit(self)
       
   205 
       
   206     def GetValue(self):
       
   207         return self.edit.GetValue()
       
   208 
       
   209     def setValidity(self, validity):
       
   210         if validity is not None:
       
   211             bmp = self.valid_bmp if validity else self.invalid_bmp
       
   212             self.validity_sbmp.SetBitmap(bmp)
       
   213             self.validity_sbmp.Show(True)
       
   214         else :
       
   215             self.validity_sbmp.Show(False)
       
   216 
       
   217 models = { typename: re.compile(regex) for typename, regex in [
       
   218     ("string", r".*"),
       
   219     ("int", r"^-?([1-9][0-9]*|0)$"),
       
   220     ("real", r"^-?([1-9][0-9]*|0)(\.[0-9]+)?$")]}
       
   221 
       
   222 class ArgEditor(ParamEditor):
       
   223     def __init__(self, parent, argdesc, prefillargdesc):
       
   224         ParamEditor.__init__(self, parent, argdesc)
       
   225         self.ParentObj = parent
       
   226         self.argdesc = argdesc
       
   227         self.Bind(wx.EVT_TEXT, self.OnArgChanged, self.edit)
       
   228         prefill = "" if prefillargdesc is None else prefillargdesc.get("value")
       
   229         self.edit.SetValue(prefill)
       
   230         # TODO add a button to add more ArgEditror instance 
       
   231         #      when ordinality is multiple
       
   232 
       
   233     def OnArgChanged(self, event):
       
   234         txt = self.edit.GetValue()
       
   235         accepts = self.argdesc.get("accepts").split(',')
       
   236         self.setValidity(
       
   237             reduce(or_,
       
   238                    map(lambda typename: 
       
   239                            models[typename].match(txt) is not None,
       
   240                        accepts), 
       
   241                    False)
       
   242             if accepts and txt else None)
       
   243         self.ParentObj.RegenSVGLater()
       
   244         event.Skip()
       
   245 
       
   246 class PathEditor(ParamEditor):
       
   247     def __init__(self, parent, pathdesc):
       
   248         ParamEditor.__init__(self, parent, pathdesc)
       
   249         self.ParentObj = parent
       
   250         self.pathdesc = pathdesc
       
   251         DropTarget = PathDropTarget(self)
       
   252         self.edit.SetDropTarget(DropTarget)
       
   253         self.edit.SetHint(_("Drag'n'drop HMI variable here"))
       
   254         self.Bind(wx.EVT_TEXT, self.OnPathChanged, self.edit)
       
   255 
       
   256     def OnHMITreeDnD(self):
       
   257         self.ParentObj.GotPathDnDOn(self)
       
   258 
       
   259     def SetPath(self, hmitree_node):
       
   260         self.edit.ChangeValue(hmitree_node.hmi_path())
       
   261         self.setValidity(
       
   262             hmitree_node.nodetype in self.pathdesc.get("accepts").split(","))
       
   263 
       
   264     def OnPathChanged(self, event):
       
   265         # TODO : find corresponding hmitre node and type to update validity
       
   266         # Lazy way : hide validity
       
   267         self.setValidity(None)
       
   268         self.ParentObj.RegenSVGLater()
       
   269         event.Skip()
       
   270     
       
   271 def KeepDoubleNewLines(txt):
       
   272     return "\n\n".join(map(
       
   273         lambda s:re.sub(r'\s+',' ',s),
       
   274         txt.split("\n\n")))
       
   275 
       
   276 _conf_key = "SVGHMIWidgetLib"
       
   277 _preview_height = 200
       
   278 _preview_margin = 5
       
   279 class WidgetLibBrowser(wx.SplitterWindow):
       
   280     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
       
   281                  size=wx.DefaultSize):
       
   282 
       
   283         wx.SplitterWindow.__init__(self, parent,
       
   284                                    style=wx.SUNKEN_BORDER | wx.SP_3D)
       
   285 
       
   286         self.bmp = None
       
   287         self.msg = None
       
   288         self.hmitree_nodes = []
       
   289         self.selected_SVG = None
       
   290 
       
   291         self.Config = wx.ConfigBase.Get()
       
   292         self.libdir = self.RecallLibDir()
       
   293         if self.libdir is None:
       
   294             self.libdir = os.path.join(ScriptDirectory, "widgetlib") 
       
   295 
       
   296         self.picker_desc_splitter = wx.SplitterWindow(self, style=wx.SUNKEN_BORDER | wx.SP_3D)
       
   297 
       
   298         self.picker_panel = wx.Panel(self.picker_desc_splitter)
       
   299         self.picker_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
       
   300         self.picker_sizer.AddGrowableCol(0)
       
   301         self.picker_sizer.AddGrowableRow(1)
       
   302 
       
   303         self.widgetpicker = WidgetPicker(self.picker_panel, self.libdir)
       
   304         self.libbutton = wx.Button(self.picker_panel, -1, _("Select SVG widget library"))
       
   305 
       
   306         self.picker_sizer.Add(self.libbutton, flag=wx.GROW)
       
   307         self.picker_sizer.Add(self.widgetpicker, flag=wx.GROW)
       
   308         self.picker_sizer.Layout()
       
   309         self.picker_panel.SetAutoLayout(True)
       
   310         self.picker_panel.SetSizer(self.picker_sizer)
       
   311 
       
   312         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
       
   313         self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton)
       
   314 
       
   315 
       
   316 
       
   317         self.main_panel = ScrolledPanel(parent=self,
       
   318                                                 name='MiscellaneousPanel',
       
   319                                                 style=wx.TAB_TRAVERSAL)
       
   320 
       
   321         self.main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0)
       
   322         self.main_sizer.AddGrowableCol(0)
       
   323         self.main_sizer.AddGrowableRow(2)
       
   324 
       
   325         self.staticmsg = wx.StaticText(self, label = _("Drag selected Widget from here to Inkscape"))
       
   326         self.preview = wx.Panel(self.main_panel, size=(-1, _preview_height + _preview_margin*2))
       
   327         self.signature_sizer = wx.BoxSizer(wx.VERTICAL)
       
   328         self.args_box = wx.StaticBox(self.main_panel, -1,
       
   329                                      _("Widget's arguments"),
       
   330                                      style = wx.ALIGN_CENTRE_HORIZONTAL)
       
   331         self.args_sizer = wx.StaticBoxSizer(self.args_box, wx.VERTICAL)
       
   332         self.paths_box = wx.StaticBox(self.main_panel, -1,
       
   333                                       _("Widget's variables"),
       
   334                                       style = wx.ALIGN_CENTRE_HORIZONTAL)
       
   335         self.paths_sizer = wx.StaticBoxSizer(self.paths_box, wx.VERTICAL)
       
   336         self.signature_sizer.Add(self.args_sizer, flag=wx.GROW)
       
   337         self.signature_sizer.AddSpacer(5)
       
   338         self.signature_sizer.Add(self.paths_sizer, flag=wx.GROW)
       
   339         self.main_sizer.Add(self.staticmsg, flag=wx.GROW)
       
   340         self.main_sizer.Add(self.preview, flag=wx.GROW)
       
   341         self.main_sizer.Add(self.signature_sizer, flag=wx.GROW)
       
   342         self.main_sizer.Layout()
       
   343         self.main_panel.SetAutoLayout(True)
       
   344         self.main_panel.SetSizer(self.main_sizer)
       
   345         self.main_sizer.Fit(self.main_panel)
       
   346         self.preview.Bind(wx.EVT_PAINT, self.OnPaint)
       
   347         self.preview.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
       
   348 
       
   349         self.desc = wx.TextCtrl(self.picker_desc_splitter, size=wx.Size(-1, 160),
       
   350                                    style=wx.TE_READONLY | wx.TE_MULTILINE)
       
   351 
       
   352         self.picker_desc_splitter.SplitHorizontally(self.picker_panel, self.desc, 400)
       
   353         self.SplitVertically(self.main_panel, self.picker_desc_splitter, 300)
       
   354 
       
   355         self.tempf = None 
       
   356 
       
   357         self.RegenSVGThread = None
       
   358         self.RegenSVGLock = Lock()
       
   359         self.RegenSVGTimer = wx.Timer(self, -1)
       
   360         self.RegenSVGParams = None
       
   361         self.Bind(wx.EVT_TIMER,
       
   362                   self.RegenSVG,
       
   363                   self.RegenSVGTimer)
       
   364 
       
   365         self.args_editors = []
       
   366         self.paths_editors = []
       
   367 
       
   368     def SetMessage(self, msg):
       
   369         self.staticmsg.SetLabel(msg)
       
   370         self.main_sizer.Layout()
       
   371 
       
   372     def ResetSignature(self):
       
   373         self.args_sizer.Clear()
       
   374         for editor in self.args_editors:
       
   375             editor.Destroy()
       
   376         self.args_editors = []
       
   377 
       
   378         self.paths_sizer.Clear()
       
   379         for editor in self.paths_editors:
       
   380             editor.Destroy()
       
   381         self.paths_editors = []
       
   382 
       
   383     def AddArgToSignature(self, arg, prefillarg):
       
   384         new_editor = ArgEditor(self, arg, prefillarg)
       
   385         self.args_editors.append(new_editor)
       
   386         self.args_sizer.Add(new_editor, flag=wx.GROW)
       
   387 
       
   388     def AddPathToSignature(self, path):
       
   389         new_editor = PathEditor(self, path)
       
   390         self.paths_editors.append(new_editor)
       
   391         self.paths_sizer.Add(new_editor, flag=wx.GROW)
       
   392 
       
   393     def GotPathDnDOn(self, target_editor):
       
   394         dndindex = self.paths_editors.index(target_editor)
       
   395 
       
   396         for hmitree_node,editor in zip(self.hmitree_nodes,
       
   397                                    self.paths_editors[dndindex:]):
       
   398             editor.SetPath(hmitree_node)
       
   399 
       
   400         self.RegenSVGNow()
       
   401 
       
   402     def RecallLibDir(self):
       
   403         conf = self.Config.Read(_conf_key)
       
   404         if len(conf) == 0:
       
   405             return None
       
   406         else:
       
   407             return DecodeFileSystemPath(conf)
       
   408 
       
   409     def RememberLibDir(self, path):
       
   410         self.Config.Write(_conf_key,
       
   411                           EncodeFileSystemPath(path))
       
   412         self.Config.Flush()
       
   413 
       
   414     def DrawPreview(self):
       
   415         """
       
   416         Refresh preview panel 
       
   417         """
       
   418         # Init preview panel paint device context
       
   419         dc = wx.PaintDC(self.preview)
       
   420         dc.Clear()
       
   421 
       
   422         if self.bmp:
       
   423             # Get Preview panel size
       
   424             sz = self.preview.GetClientSize()
       
   425             w = self.bmp.GetWidth()
       
   426             dc.DrawBitmap(self.bmp, (sz.width - w)/2, _preview_margin)
       
   427 
       
   428 
       
   429 
       
   430     def OnSelectLibDir(self, event):
       
   431         defaultpath = self.RecallLibDir()
       
   432         if defaultpath == None:
       
   433             defaultpath = os.path.expanduser("~")
       
   434 
       
   435         dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath,
       
   436                               style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
       
   437 
       
   438         if dialog.ShowModal() == wx.ID_OK:
       
   439             self.libdir = dialog.GetPath()
       
   440             self.RememberLibDir(self.libdir)
       
   441             self.widgetpicker.MakeTree(self.libdir)
       
   442 
       
   443         dialog.Destroy()
       
   444 
       
   445     def OnPaint(self, event):
       
   446         """
       
   447         Called when Preview panel needs to be redrawn
       
   448         @param event: wx.PaintEvent
       
   449         """
       
   450         self.DrawPreview()
       
   451         event.Skip()
       
   452 
       
   453     def GenThumbnail(self, svgpath, thumbpath):
       
   454         inkpath = get_inkscape_path()
       
   455         if inkpath is None:
       
   456             self.msg = _("Inkscape is not installed.")
       
   457             return False
       
   458         # TODO: spawn a thread, to decouple thumbnail gen
       
   459         status, result, _err_result = ProcessLogger(
       
   460             None,
       
   461             '"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath +
       
   462             '" -D -h ' + str(_preview_height)).spin()
       
   463         if status != 0:
       
   464             self.msg = _("Inkscape couldn't generate thumbnail.")
       
   465             return False
       
   466         return True
       
   467 
       
   468     def OnWidgetSelection(self, event):
       
   469         """
       
   470         Called when tree item is selected
       
   471         @param event: wx.TreeEvent
       
   472         """
       
   473         item_pydata = self.widgetpicker.GetPyData(event.GetItem())
       
   474         if item_pydata is not None:
       
   475             svgpath = item_pydata
       
   476             dname = os.path.dirname(svgpath)
       
   477             fname = os.path.basename(svgpath)
       
   478             hasher = hashlib.new('md5')
       
   479             with open(svgpath, 'rb') as afile:
       
   480                 while True:
       
   481                     buf = afile.read(65536)
       
   482                     if len(buf) > 0:
       
   483                         hasher.update(buf)
       
   484                     else:
       
   485                         break
       
   486             digest = hasher.hexdigest()
       
   487             thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png"
       
   488             thumbdir = os.path.join(dname, ".svghmithumbs") 
       
   489             thumbpath = os.path.join(thumbdir, thumbfname) 
       
   490 
       
   491             have_thumb = os.path.exists(thumbpath)
       
   492 
       
   493             try:
       
   494                 if not have_thumb:
       
   495                     if not os.path.exists(thumbdir):
       
   496                         os.mkdir(thumbdir)
       
   497                     have_thumb = self.GenThumbnail(svgpath, thumbpath)
       
   498 
       
   499                 self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
       
   500 
       
   501                 self.selected_SVG = svgpath if have_thumb else None
       
   502 
       
   503                 self.AnalyseWidgetAndUpdateUI(fname)
       
   504 
       
   505                 self.SetMessage(self.msg)
       
   506 
       
   507             except IOError:
       
   508                 self.msg = _("Widget library must be writable")
       
   509 
       
   510             self.Refresh()
       
   511         event.Skip()
       
   512 
       
   513     def OnHMITreeNodeSelection(self, hmitree_nodes):
       
   514         self.hmitree_nodes = hmitree_nodes
       
   515 
       
   516     def OnLeftDown(self, evt):
       
   517         if self.tempf is not None:
       
   518             filename = self.tempf.name
       
   519             data = wx.FileDataObject()
       
   520             data.AddFile(filename)
       
   521             dropSource = wx.DropSource(self)
       
   522             dropSource.SetData(data)
       
   523             dropSource.DoDragDrop(wx.Drag_AllowMove)
       
   524 
       
   525     def RegenSVGLater(self, when=1):
       
   526         self.SetMessage(_("SVG generation pending"))
       
   527         self.RegenSVGTimer.Start(milliseconds=when*1000, oneShot=True)
       
   528 
       
   529     def RegenSVGNow(self):
       
   530         self.RegenSVGLater(when=0)
       
   531 
       
   532     def RegenSVG(self, event):
       
   533         self.SetMessage(_("Generating SVG..."))
       
   534         args = [arged.GetValue() for arged in self.args_editors]
       
   535         while args and not args[-1]: args.pop(-1)
       
   536         paths = [pathed.GetValue() for pathed in self.paths_editors]
       
   537         while paths and not paths[-1]: paths.pop(-1)
       
   538         if self.RegenSVGLock.acquire(True):
       
   539             self.RegenSVGParams = (args, paths)
       
   540             if self.RegenSVGThread is None:
       
   541                 self.RegenSVGThread = \
       
   542                     Thread(target=self.RegenSVGProc,
       
   543                            name="RegenSVGThread").start()
       
   544             self.RegenSVGLock.release()
       
   545         event.Skip()
       
   546 
       
   547     def RegenSVGProc(self):
       
   548         self.RegenSVGLock.acquire(True)
       
   549 
       
   550         newparams = self.RegenSVGParams
       
   551         self.RegenSVGParams = None
       
   552 
       
   553         while newparams is not None:
       
   554             self.RegenSVGLock.release()
       
   555 
       
   556             res = self.GenDnDSVG(newparams)
       
   557 
       
   558             self.RegenSVGLock.acquire(True)
       
   559 
       
   560             newparams = self.RegenSVGParams
       
   561             self.RegenSVGParams = None
       
   562 
       
   563         self.RegenSVGThread = None
       
   564 
       
   565         self.RegenSVGLock.release()
       
   566 
       
   567         wx.CallAfter(self.DoneRegenSVG)
       
   568         
       
   569     def DoneRegenSVG(self):
       
   570         self.SetMessage(self.msg if self.msg else _("SVG ready for drag'n'drop"))
       
   571         
       
   572     def AnalyseWidgetAndUpdateUI(self, fname):
       
   573         self.msg = ""
       
   574         self.ResetSignature()
       
   575 
       
   576         try:
       
   577             if self.selected_SVG is None:
       
   578                 raise Exception(_("No widget selected"))
       
   579 
       
   580             transform = XSLTransform(
       
   581                 os.path.join(ScriptDirectory, "analyse_widget.xslt"),[])
       
   582 
       
   583             svgdom = etree.parse(self.selected_SVG)
       
   584 
       
   585             signature = transform.transform(svgdom)
       
   586 
       
   587             for entry in transform.get_error_log():
       
   588                 self.msg += "XSLT: " + entry.message + "\n" 
       
   589 
       
   590         except Exception as e:
       
   591             self.msg += str(e)
       
   592             return
       
   593         except XSLTApplyError as e:
       
   594             self.msg += "Widget " + fname + " analysis error: " + e.message
       
   595             return
       
   596             
       
   597         self.msg += "Widget " + fname + ": OK"
       
   598 
       
   599         widgets = signature.getroot()
       
   600         widget = widgets.find("widget")
       
   601         defs = widget.find("defs")
       
   602         # Keep double newlines (to mark paragraphs)
       
   603         widget_desc = widget.find("desc")
       
   604         self.desc.SetValue(
       
   605             fname + ":\n\n" + (
       
   606                 _("No description given") if widget_desc is None else 
       
   607                 KeepDoubleNewLines(widget_desc.text)
       
   608             ) + "\n\n" +
       
   609             defs.find("type").text + " Widget: "+defs.find("shortdesc").text+"\n\n" +
       
   610             KeepDoubleNewLines(defs.find("longdesc").text))
       
   611         prefillargs = widget.findall("arg")
       
   612         args = defs.findall("arg")
       
   613         # extend args description in prefilled args in longer 
       
   614         # (case of variable list of args)
       
   615         if len(prefillargs) < len(args):
       
   616             prefillargs += [None]*(len(args)-len(prefillargs))
       
   617         if args and len(prefillargs) > len(args):
       
   618             # TODO: check ordinality of last arg
       
   619             # TODO: check that only last arg has multiple ordinality
       
   620             args += [args[-1]]*(len(prefillargs)-len(args))
       
   621         self.args_box.Show(len(args)!=0)
       
   622         for arg, prefillarg in izip(args,prefillargs):
       
   623             self.AddArgToSignature(arg, prefillarg)
       
   624         paths = defs.findall("path")
       
   625         self.paths_box.Show(len(paths)!=0)
       
   626         for path in paths:
       
   627             self.AddPathToSignature(path)
       
   628 
       
   629         for widget in widgets:
       
   630             widget_type = widget.get("type")
       
   631             for path in widget.iterchildren("path"):
       
   632                 path_value = path.get("value")
       
   633                 path_accepts = map(
       
   634                     str.strip, path.get("accepts", '')[1:-1].split(','))
       
   635 
       
   636         self.main_panel.SetupScrolling(scroll_x=False)
       
   637 
       
   638     def GetWidgetParams(self, _context):
       
   639         args,paths = self.GenDnDSVGParams
       
   640         root = etree.Element("params")
       
   641         for arg in args:
       
   642             etree.SubElement(root, "arg", value=arg)
       
   643         for path in paths:
       
   644             etree.SubElement(root, "path", value=path)
       
   645         return root
       
   646 
       
   647 
       
   648     def GenDnDSVG(self, newparams):
       
   649         self.msg = ""
       
   650 
       
   651         self.GenDnDSVGParams = newparams
       
   652 
       
   653         if self.tempf is not None:
       
   654             os.unlink(self.tempf.name)
       
   655             self.tempf = None
       
   656 
       
   657         try:
       
   658             if self.selected_SVG is None:
       
   659                 raise Exception(_("No widget selected"))
       
   660 
       
   661             transform = XSLTransform(
       
   662                 os.path.join(ScriptDirectory, "gen_dnd_widget_svg.xslt"),
       
   663                 [("GetWidgetParams", self.GetWidgetParams)])
       
   664 
       
   665             svgdom = etree.parse(self.selected_SVG)
       
   666 
       
   667             result = transform.transform(svgdom)
       
   668 
       
   669             for entry in transform.get_error_log():
       
   670                 self.msg += "XSLT: " + entry.message + "\n" 
       
   671 
       
   672             self.tempf = NamedTemporaryFile(suffix='.svg', delete=False)
       
   673             result.write(self.tempf, encoding="utf-8")
       
   674             self.tempf.close()
       
   675 
       
   676         except Exception as e:
       
   677             self.msg += str(e)
       
   678         except XSLTApplyError as e:
       
   679             self.msg += "Widget transform error: " + e.message
       
   680                 
       
   681     def __del__(self):
       
   682         if self.tempf is not None:
       
   683             os.unlink(self.tempf.name)
       
   684 
       
   685 class SVGHMI_UI(wx.SplitterWindow):
       
   686 
       
   687     def __init__(self, parent, register_for_HMI_tree_updates):
       
   688         wx.SplitterWindow.__init__(self, parent,
       
   689                                    style=wx.SUNKEN_BORDER | wx.SP_3D)
       
   690 
       
   691         self.SelectionTree = HMITreeSelector(self)
       
   692         self.Staging = WidgetLibBrowser(self)
       
   693         self.SplitVertically(self.SelectionTree, self.Staging, 300)
       
   694         register_for_HMI_tree_updates(weakref.ref(self))
       
   695 
       
   696     def HMITreeUpdate(self, hmi_tree_root):
       
   697         self.SelectionTree.MakeTree(hmi_tree_root)
       
   698 
       
   699     def OnHMITreeNodeSelection(self, hmitree_nodes):
       
   700         self.Staging.OnHMITreeNodeSelection(hmitree_nodes)