235 # ^ |
235 # ^ |
236 # note the double zero after "runtime_", |
236 # note the double zero after "runtime_", |
237 # to ensure placement before other CTN generated code in execution order |
237 # to ensure placement before other CTN generated code in execution order |
238 |
238 |
239 |
239 |
240 def SVGHMIEditorUpdater(ref): |
240 def Register_SVGHMI_UI_for_HMI_tree_updates(ref): |
241 def SVGHMIEditorUpdate(): |
241 global on_hmitree_update |
242 o = ref() |
242 def HMITreeUpdate(_hmi_tree_root): |
243 if o is not None: |
243 obj = ref() |
244 wx.CallAfter(o.MakeTree) |
244 if obj is not None: |
245 return SVGHMIEditorUpdate |
245 obj.HMITreeUpdate(_hmi_tree_root) |
246 |
246 |
247 class HMITreeSelector(wx.TreeCtrl): |
247 on_hmitree_update = HMITreeUpdate |
248 def __init__(self, parent): |
|
249 global on_hmitree_update |
|
250 wx.TreeCtrl.__init__(self, parent, style=( |
|
251 wx.TR_MULTIPLE | |
|
252 wx.TR_HAS_BUTTONS | |
|
253 wx.SUNKEN_BORDER | |
|
254 wx.TR_LINES_AT_ROOT)) |
|
255 |
|
256 on_hmitree_update = SVGHMIEditorUpdater(weakref.ref(self)) |
|
257 self.MakeTree() |
|
258 |
|
259 def _recurseTree(self, current_hmitree_root, current_tc_root): |
|
260 for c in current_hmitree_root.children: |
|
261 if hasattr(c, "children"): |
|
262 display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \ |
|
263 if c.hmiclass is not None else c.name |
|
264 tc_child = self.AppendItem(current_tc_root, display_name) |
|
265 self.SetPyData(tc_child, None) # TODO |
|
266 |
|
267 self._recurseTree(c,tc_child) |
|
268 else: |
|
269 display_name = '{} {}'.format(c.nodetype[4:], c.name) |
|
270 tc_child = self.AppendItem(current_tc_root, display_name) |
|
271 self.SetPyData(tc_child, None) # TODO |
|
272 |
|
273 def MakeTree(self): |
|
274 global hmi_tree_root |
|
275 |
|
276 self.Freeze() |
|
277 |
|
278 self.root = None |
|
279 self.DeleteAllItems() |
|
280 |
|
281 root_display_name = _("Please build to see HMI Tree") \ |
|
282 if hmi_tree_root is None else "HMI" |
|
283 self.root = self.AddRoot(root_display_name) |
|
284 self.SetPyData(self.root, None) |
|
285 |
|
286 if hmi_tree_root is not None: |
|
287 self._recurseTree(hmi_tree_root, self.root) |
|
288 self.Expand(self.root) |
|
289 |
|
290 self.Thaw() |
|
291 |
|
292 class WidgetPicker(wx.TreeCtrl): |
|
293 def __init__(self, parent, initialdir=None): |
|
294 wx.TreeCtrl.__init__(self, parent, style=( |
|
295 wx.TR_MULTIPLE | |
|
296 wx.TR_HAS_BUTTONS | |
|
297 wx.SUNKEN_BORDER | |
|
298 wx.TR_LINES_AT_ROOT)) |
|
299 |
|
300 self.MakeTree(initialdir) |
|
301 |
|
302 def _recurseTree(self, current_dir, current_tc_root, dirlist): |
|
303 """ |
|
304 recurse through subdirectories, but creates tree nodes |
|
305 only when (sub)directory conbtains .svg file |
|
306 """ |
|
307 res = [] |
|
308 for f in sorted(os.listdir(current_dir)): |
|
309 p = os.path.join(current_dir,f) |
|
310 if os.path.isdir(p): |
|
311 |
|
312 r = self._recurseTree(p, current_tc_root, dirlist + [f]) |
|
313 if len(r) > 0 : |
|
314 res = r |
|
315 dirlist = [] |
|
316 current_tc_root = res.pop() |
|
317 |
|
318 elif os.path.splitext(f)[1].upper() == ".SVG": |
|
319 if len(dirlist) > 0 : |
|
320 res = [] |
|
321 for d in dirlist: |
|
322 current_tc_root = self.AppendItem(current_tc_root, d) |
|
323 res.append(current_tc_root) |
|
324 self.SetPyData(current_tc_root, None) |
|
325 dirlist = [] |
|
326 res.pop() |
|
327 tc_child = self.AppendItem(current_tc_root, f) |
|
328 self.SetPyData(tc_child, p) |
|
329 return res |
|
330 |
|
331 def MakeTree(self, lib_dir = None): |
|
332 global hmi_tree_root |
|
333 |
|
334 self.Freeze() |
|
335 |
|
336 self.root = None |
|
337 self.DeleteAllItems() |
|
338 |
|
339 root_display_name = _("Please select widget library directory") \ |
|
340 if lib_dir is None else os.path.basename(lib_dir) |
|
341 self.root = self.AddRoot(root_display_name) |
|
342 self.SetPyData(self.root, None) |
|
343 |
|
344 if lib_dir is not None: |
|
345 self._recurseTree(lib_dir, self.root, []) |
|
346 self.Expand(self.root) |
|
347 |
|
348 self.Thaw() |
|
349 |
|
350 _conf_key = "SVGHMIWidgetLib" |
|
351 _preview_height = 200 |
|
352 class WidgetLibBrowser(wx.Panel): |
|
353 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, |
|
354 size=wx.DefaultSize): |
|
355 |
|
356 wx.Panel.__init__(self, parent, id, pos, size) |
|
357 |
|
358 self.bmp = None |
|
359 self.msg = None |
|
360 self.hmitree_node = None |
|
361 self.selected_SVG = None |
|
362 |
|
363 self.Config = wx.ConfigBase.Get() |
|
364 self.libdir = self.RecallLibDir() |
|
365 |
|
366 sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0) |
|
367 sizer.AddGrowableCol(0) |
|
368 sizer.AddGrowableRow(1) |
|
369 self.libbutton = wx.Button(self, -1, _("Select SVG widget library")) |
|
370 self.widgetpicker = WidgetPicker(self, self.libdir) |
|
371 self.preview = wx.Panel(self, size=(-1, _preview_height + 10)) #, style=wx.SIMPLE_BORDER) |
|
372 #self.preview.SetBackgroundColour(wx.WHITE) |
|
373 sizer.AddWindow(self.libbutton, flag=wx.GROW) |
|
374 sizer.AddWindow(self.widgetpicker, flag=wx.GROW) |
|
375 sizer.AddWindow(self.preview, flag=wx.GROW) |
|
376 sizer.Layout() |
|
377 self.SetAutoLayout(True) |
|
378 self.SetSizer(sizer) |
|
379 sizer.Fit(self) |
|
380 self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton) |
|
381 self.preview.Bind(wx.EVT_PAINT, self.OnPaint) |
|
382 |
|
383 self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker) |
|
384 |
|
385 self.msg = _("Drag selected Widget from here to Inkscape") |
|
386 |
|
387 def RecallLibDir(self): |
|
388 conf = self.Config.Read(_conf_key) |
|
389 if len(conf) == 0: |
|
390 return None |
|
391 else: |
|
392 return DecodeFileSystemPath(conf) |
|
393 |
|
394 def RememberLibDir(self, path): |
|
395 self.Config.Write(_conf_key, |
|
396 EncodeFileSystemPath(path)) |
|
397 self.Config.Flush() |
|
398 |
|
399 def DrawPreview(self): |
|
400 """ |
|
401 Refresh preview panel |
|
402 """ |
|
403 # Init preview panel paint device context |
|
404 dc = wx.PaintDC(self.preview) |
|
405 dc.Clear() |
|
406 |
|
407 if self.bmp: |
|
408 # Get Preview panel size |
|
409 sz = self.preview.GetClientSize() |
|
410 w = self.bmp.GetWidth() |
|
411 dc.DrawBitmap(self.bmp, (sz.width - w)/2, 5) |
|
412 |
|
413 if self.msg: |
|
414 dc.SetFont(self.GetFont()) |
|
415 dc.DrawText(self.msg, 25,25) |
|
416 |
|
417 |
|
418 def OnSelectLibDir(self, event): |
|
419 defaultpath = self.RecallLibDir() |
|
420 if defaultpath == None: |
|
421 defaultpath = os.path.expanduser("~") |
|
422 |
|
423 dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath, |
|
424 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) |
|
425 |
|
426 if dialog.ShowModal() == wx.ID_OK: |
|
427 self.libdir = dialog.GetPath() |
|
428 self.RememberLibDir(self.libdir) |
|
429 self.widgetpicker.MakeTree(self.libdir) |
|
430 |
|
431 dialog.Destroy() |
|
432 |
|
433 def OnPaint(self, event): |
|
434 """ |
|
435 Called when Preview panel needs to be redrawn |
|
436 @param event: wx.PaintEvent |
|
437 """ |
|
438 self.DrawPreview() |
|
439 event.Skip() |
|
440 |
|
441 def GenThumbnail(self, svgpath, thumbpath): |
|
442 inkpath = get_inkscape_path() |
|
443 if inkpath is None: |
|
444 self.msg = _("Inkscape is not installed.") |
|
445 return False |
|
446 # TODO: spawn a thread, to decouple thumbnail gen |
|
447 status, result, _err_result = ProcessLogger( |
|
448 None, |
|
449 '"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath + |
|
450 '" -D -h ' + str(_preview_height)).spin() |
|
451 if status != 0: |
|
452 self.msg = _("Inkscape couldn't generate thumbnail.") |
|
453 return False |
|
454 return True |
|
455 |
|
456 def OnWidgetSelection(self, event): |
|
457 """ |
|
458 Called when tree item is selected |
|
459 @param event: wx.TreeEvent |
|
460 """ |
|
461 item_pydata = self.widgetpicker.GetPyData(event.GetItem()) |
|
462 if item_pydata is not None: |
|
463 svgpath = item_pydata |
|
464 dname = os.path.dirname(svgpath) |
|
465 fname = os.path.basename(svgpath) |
|
466 hasher = hashlib.new('md5') |
|
467 with open(svgpath, 'rb') as afile: |
|
468 while True: |
|
469 buf = afile.read(65536) |
|
470 if len(buf) > 0: |
|
471 hasher.update(buf) |
|
472 else: |
|
473 break |
|
474 digest = hasher.hexdigest() |
|
475 thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png" |
|
476 thumbdir = os.path.join(dname, ".svghmithumbs") |
|
477 thumbpath = os.path.join(thumbdir, thumbfname) |
|
478 |
|
479 self.msg = None |
|
480 have_thumb = os.path.exists(thumbpath) |
|
481 |
|
482 if not have_thumb: |
|
483 try: |
|
484 if not os.path.exists(thumbdir): |
|
485 os.mkdir(thumbdir) |
|
486 except IOError: |
|
487 self.msg = _("Widget library must be writable") |
|
488 else: |
|
489 have_thumb = self.GenThumbnail(svgpath, thumbpath) |
|
490 |
|
491 self.bmp = wx.Bitmap(thumbpath) if have_thumb else None |
|
492 |
|
493 self.selected_SVG = svgpath if have_thumb else None |
|
494 self.ValidateWidget() |
|
495 |
|
496 self.Refresh() |
|
497 event.Skip() |
|
498 |
|
499 def OnHMITreeNodeSelection(self, hmitree_node): |
|
500 self.hmitree_node = hmitree_node |
|
501 self.ValidateWidget() |
|
502 self.Refresh() |
|
503 |
|
504 def ValidateWidget(self): |
|
505 if self.selected_SVG is not None: |
|
506 if self.hmitree_node is not None: |
|
507 pass |
|
508 # XXX TODO: |
|
509 # - check SVG is valid for selected HMI tree item |
|
510 # - prepare for D'n'D |
|
511 |
|
512 |
|
513 class HMITreeView(wx.SplitterWindow): |
|
514 |
|
515 def __init__(self, parent): |
|
516 wx.SplitterWindow.__init__(self, parent, |
|
517 style=wx.SUNKEN_BORDER | wx.SP_3D) |
|
518 |
|
519 self.SelectionTree = HMITreeSelector(self) |
|
520 self.Staging = WidgetLibBrowser(self) |
|
521 self.SplitVertically(self.SelectionTree, self.Staging, 300) |
|
522 |
248 |
523 |
249 |
524 class SVGHMIEditor(ConfTreeNodeEditor): |
250 class SVGHMIEditor(ConfTreeNodeEditor): |
525 CONFNODEEDITOR_TABS = [ |
251 CONFNODEEDITOR_TABS = [ |
526 (_("HMI Tree"), "CreateHMITreeView")] |
252 (_("HMI Tree"), "CreateSVGHMI_UI")] |
527 |
253 |
528 def CreateHMITreeView(self, parent): |
254 def CreateSVGHMI_UI(self, parent): |
529 global hmi_tree_root |
255 global hmi_tree_root |
530 |
256 |
531 if hmi_tree_root is None: |
257 if hmi_tree_root is None: |
532 buildpath = self.Controler.GetCTRoot()._getBuildPath() |
258 buildpath = self.Controler.GetCTRoot()._getBuildPath() |
533 hmitree_backup_path = os.path.join(buildpath, "hmitree.xml") |
259 hmitree_backup_path = os.path.join(buildpath, "hmitree.xml") |
534 if os.path.exists(hmitree_backup_path): |
260 if os.path.exists(hmitree_backup_path): |
535 hmitree_backup_file = open(hmitree_backup_path, 'rb') |
261 hmitree_backup_file = open(hmitree_backup_path, 'rb') |
536 hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot()) |
262 hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot()) |
537 |
263 |
538 |
264 return SVGHMI_UI(parent, Register_SVGHMI_UI_for_HMI_tree_updates) |
539 #self.HMITreeView = HMITreeView(self) |
|
540 #return HMITreeSelector(parent) |
|
541 return HMITreeView(parent) |
|
542 |
265 |
543 class SVGHMI(object): |
266 class SVGHMI(object): |
544 XSD = """<?xml version="1.0" encoding="utf-8" ?> |
267 XSD = """<?xml version="1.0" encoding="utf-8" ?> |
545 <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
268 <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
546 <xsd:element name="SVGHMI"> |
269 <xsd:element name="SVGHMI"> |