svghmi/ui.py
branchsvghmi
changeset 3201 6dadc1690284
parent 3197 0f41c1e2c121
child 3208 b5330d76e225
equal deleted inserted replaced
3197:0f41c1e2c121 3201:6dadc1690284
       
     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 
       
    14 import wx
       
    15 
       
    16 from IDEFrame import EncodeFileSystemPath, DecodeFileSystemPath
       
    17 from docutil import get_inkscape_path
       
    18 
       
    19 from util.ProcessLogger import ProcessLogger
       
    20 
       
    21 def SVGHMIEditorUpdater(ref):
       
    22     def SVGHMIEditorUpdate():
       
    23         o = ref()
       
    24         if o is not None:
       
    25             wx.CallAfter(o.MakeTree)
       
    26     return SVGHMIEditorUpdate
       
    27 
       
    28 class HMITreeSelector(wx.TreeCtrl):
       
    29     def __init__(self, parent):
       
    30         global on_hmitree_update
       
    31         wx.TreeCtrl.__init__(self, parent, style=(
       
    32             wx.TR_MULTIPLE |
       
    33             wx.TR_HAS_BUTTONS |
       
    34             wx.SUNKEN_BORDER |
       
    35             wx.TR_LINES_AT_ROOT))
       
    36 
       
    37         on_hmitree_update = SVGHMIEditorUpdater(weakref.ref(self))
       
    38         self.MakeTree()
       
    39 
       
    40     def _recurseTree(self, current_hmitree_root, current_tc_root):
       
    41         for c in current_hmitree_root.children:
       
    42             if hasattr(c, "children"):
       
    43                 display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \
       
    44                                if c.hmiclass is not None else c.name
       
    45                 tc_child = self.AppendItem(current_tc_root, display_name)
       
    46                 self.SetPyData(tc_child, None) # TODO
       
    47 
       
    48                 self._recurseTree(c,tc_child)
       
    49             else:
       
    50                 display_name = '{} {}'.format(c.nodetype[4:], c.name)
       
    51                 tc_child = self.AppendItem(current_tc_root, display_name)
       
    52                 self.SetPyData(tc_child, None) # TODO
       
    53 
       
    54     def MakeTree(self, hmi_tree_root=None):
       
    55 
       
    56         self.Freeze()
       
    57 
       
    58         self.root = None
       
    59         self.DeleteAllItems()
       
    60 
       
    61         root_display_name = _("Please build to see HMI Tree") \
       
    62             if hmi_tree_root is None else "HMI"
       
    63         self.root = self.AddRoot(root_display_name)
       
    64         self.SetPyData(self.root, None)
       
    65 
       
    66         if hmi_tree_root is not None:
       
    67             self._recurseTree(hmi_tree_root, self.root)
       
    68             self.Expand(self.root)
       
    69 
       
    70         self.Thaw()
       
    71 
       
    72 class WidgetPicker(wx.TreeCtrl):
       
    73     def __init__(self, parent, initialdir=None):
       
    74         wx.TreeCtrl.__init__(self, parent, style=(
       
    75             wx.TR_MULTIPLE |
       
    76             wx.TR_HAS_BUTTONS |
       
    77             wx.SUNKEN_BORDER |
       
    78             wx.TR_LINES_AT_ROOT))
       
    79 
       
    80         self.MakeTree(initialdir)
       
    81 
       
    82     def _recurseTree(self, current_dir, current_tc_root, dirlist):
       
    83         """
       
    84         recurse through subdirectories, but creates tree nodes 
       
    85         only when (sub)directory conbtains .svg file
       
    86         """
       
    87         res = []
       
    88         for f in sorted(os.listdir(current_dir)):
       
    89             p = os.path.join(current_dir,f)
       
    90             if os.path.isdir(p):
       
    91 
       
    92                 r = self._recurseTree(p, current_tc_root, dirlist + [f])
       
    93                 if len(r) > 0 :
       
    94                     res = r
       
    95                     dirlist = []
       
    96                     current_tc_root = res.pop()
       
    97 
       
    98             elif os.path.splitext(f)[1].upper() == ".SVG":
       
    99                 if len(dirlist) > 0 :
       
   100                     res = []
       
   101                     for d in dirlist:
       
   102                         current_tc_root = self.AppendItem(current_tc_root, d)
       
   103                         res.append(current_tc_root)
       
   104                         self.SetPyData(current_tc_root, None)
       
   105                     dirlist = []
       
   106                     res.pop()
       
   107                 tc_child = self.AppendItem(current_tc_root, f)
       
   108                 self.SetPyData(tc_child, p)
       
   109         return res
       
   110 
       
   111     def MakeTree(self, lib_dir = None):
       
   112 
       
   113         self.Freeze()
       
   114 
       
   115         self.root = None
       
   116         self.DeleteAllItems()
       
   117 
       
   118         root_display_name = _("Please select widget library directory") \
       
   119             if lib_dir is None else os.path.basename(lib_dir)
       
   120         self.root = self.AddRoot(root_display_name)
       
   121         self.SetPyData(self.root, None)
       
   122 
       
   123         if lib_dir is not None:
       
   124             self._recurseTree(lib_dir, self.root, [])
       
   125             self.Expand(self.root)
       
   126 
       
   127         self.Thaw()
       
   128 
       
   129 _conf_key = "SVGHMIWidgetLib"
       
   130 _preview_height = 200
       
   131 class WidgetLibBrowser(wx.Panel):
       
   132     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
       
   133                  size=wx.DefaultSize):
       
   134 
       
   135         wx.Panel.__init__(self, parent, id, pos, size)     
       
   136 
       
   137         self.bmp = None
       
   138         self.msg = None
       
   139         self.hmitree_node = None
       
   140         self.selected_SVG = None
       
   141 
       
   142         self.Config = wx.ConfigBase.Get()
       
   143         self.libdir = self.RecallLibDir()
       
   144 
       
   145         sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0)
       
   146         sizer.AddGrowableCol(0)
       
   147         sizer.AddGrowableRow(1)
       
   148         self.libbutton = wx.Button(self, -1, _("Select SVG widget library"))
       
   149         self.widgetpicker = WidgetPicker(self, self.libdir)
       
   150         self.preview = wx.Panel(self, size=(-1, _preview_height + 10))  #, style=wx.SIMPLE_BORDER)
       
   151         #self.preview.SetBackgroundColour(wx.WHITE)
       
   152         sizer.AddWindow(self.libbutton, flag=wx.GROW)
       
   153         sizer.AddWindow(self.widgetpicker, flag=wx.GROW)
       
   154         sizer.AddWindow(self.preview, flag=wx.GROW)
       
   155         sizer.Layout()
       
   156         self.SetAutoLayout(True)
       
   157         self.SetSizer(sizer)
       
   158         sizer.Fit(self)
       
   159         self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton)
       
   160         self.preview.Bind(wx.EVT_PAINT, self.OnPaint)
       
   161 
       
   162         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
       
   163 
       
   164         self.msg = _("Drag selected Widget from here to Inkscape")
       
   165 
       
   166     def RecallLibDir(self):
       
   167         conf = self.Config.Read(_conf_key)
       
   168         if len(conf) == 0:
       
   169             return None
       
   170         else:
       
   171             return DecodeFileSystemPath(conf)
       
   172 
       
   173     def RememberLibDir(self, path):
       
   174         self.Config.Write(_conf_key,
       
   175                           EncodeFileSystemPath(path))
       
   176         self.Config.Flush()
       
   177 
       
   178     def DrawPreview(self):
       
   179         """
       
   180         Refresh preview panel 
       
   181         """
       
   182         # Init preview panel paint device context
       
   183         dc = wx.PaintDC(self.preview)
       
   184         dc.Clear()
       
   185 
       
   186         if self.bmp:
       
   187             # Get Preview panel size
       
   188             sz = self.preview.GetClientSize()
       
   189             w = self.bmp.GetWidth()
       
   190             dc.DrawBitmap(self.bmp, (sz.width - w)/2, 5)
       
   191 
       
   192         if self.msg:
       
   193             dc.SetFont(self.GetFont())
       
   194             dc.DrawText(self.msg, 25,25)
       
   195 
       
   196 
       
   197     def OnSelectLibDir(self, event):
       
   198         defaultpath = self.RecallLibDir()
       
   199         if defaultpath == None:
       
   200             defaultpath = os.path.expanduser("~")
       
   201 
       
   202         dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath,
       
   203                               style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
       
   204 
       
   205         if dialog.ShowModal() == wx.ID_OK:
       
   206             self.libdir = dialog.GetPath()
       
   207             self.RememberLibDir(self.libdir)
       
   208             self.widgetpicker.MakeTree(self.libdir)
       
   209 
       
   210         dialog.Destroy()
       
   211 
       
   212     def OnPaint(self, event):
       
   213         """
       
   214         Called when Preview panel needs to be redrawn
       
   215         @param event: wx.PaintEvent
       
   216         """
       
   217         self.DrawPreview()
       
   218         event.Skip()
       
   219 
       
   220     def GenThumbnail(self, svgpath, thumbpath):
       
   221         inkpath = get_inkscape_path()
       
   222         if inkpath is None:
       
   223             self.msg = _("Inkscape is not installed.")
       
   224             return False
       
   225         # TODO: spawn a thread, to decouple thumbnail gen
       
   226         status, result, _err_result = ProcessLogger(
       
   227             None,
       
   228             '"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath +
       
   229             '" -D -h ' + str(_preview_height)).spin()
       
   230         if status != 0:
       
   231             self.msg = _("Inkscape couldn't generate thumbnail.")
       
   232             return False
       
   233         return True
       
   234 
       
   235     def OnWidgetSelection(self, event):
       
   236         """
       
   237         Called when tree item is selected
       
   238         @param event: wx.TreeEvent
       
   239         """
       
   240         item_pydata = self.widgetpicker.GetPyData(event.GetItem())
       
   241         if item_pydata is not None:
       
   242             svgpath = item_pydata
       
   243             dname = os.path.dirname(svgpath)
       
   244             fname = os.path.basename(svgpath)
       
   245             hasher = hashlib.new('md5')
       
   246             with open(svgpath, 'rb') as afile:
       
   247                 while True:
       
   248                     buf = afile.read(65536)
       
   249                     if len(buf) > 0:
       
   250                         hasher.update(buf)
       
   251                     else:
       
   252                         break
       
   253             digest = hasher.hexdigest()
       
   254             thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png"
       
   255             thumbdir = os.path.join(dname, ".svghmithumbs") 
       
   256             thumbpath = os.path.join(thumbdir, thumbfname) 
       
   257 
       
   258             self.msg = None
       
   259             have_thumb = os.path.exists(thumbpath)
       
   260 
       
   261             if not have_thumb:
       
   262                 try:
       
   263                     if not os.path.exists(thumbdir):
       
   264                         os.mkdir(thumbdir)
       
   265                 except IOError:
       
   266                     self.msg = _("Widget library must be writable")
       
   267                 else:
       
   268                     have_thumb = self.GenThumbnail(svgpath, thumbpath)
       
   269 
       
   270             self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
       
   271 
       
   272             self.selected_SVG = svgpath if have_thumb else None
       
   273             self.ValidateWidget()
       
   274 
       
   275             self.Refresh()
       
   276         event.Skip()
       
   277 
       
   278     def OnHMITreeNodeSelection(self, hmitree_node):
       
   279         self.hmitree_node = hmitree_node
       
   280         self.ValidateWidget()
       
   281         self.Refresh()
       
   282 
       
   283     def ValidateWidget(self):
       
   284         if self.selected_SVG is not None:
       
   285             if self.hmitree_node is not None:
       
   286                 pass
       
   287         # XXX TODO: 
       
   288         #      - check SVG is valid for selected HMI tree item
       
   289         #      - prepare for D'n'D
       
   290 
       
   291 
       
   292 class SVGHMI_UI(wx.SplitterWindow):
       
   293 
       
   294     def __init__(self, parent, register_for_HMI_tree_updates):
       
   295         wx.SplitterWindow.__init__(self, parent,
       
   296                                    style=wx.SUNKEN_BORDER | wx.SP_3D)
       
   297 
       
   298         self.SelectionTree = HMITreeSelector(self)
       
   299         self.Staging = WidgetLibBrowser(self)
       
   300         self.SplitVertically(self.SelectionTree, self.Staging, 300)
       
   301         register_for_HMI_tree_updates(weakref.ref(self))
       
   302 
       
   303     def HMITreeUpdate(self, hmi_tree_root):
       
   304             self.SelectionTree.MakeTree(hmi_tree_root)
       
   305