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