|
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 |