diff -r 577118ebd179 -r c89fc366bebd svghmi/svghmi.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/svghmi.py Thu Sep 02 21:36:29 2021 +0200
@@ -0,0 +1,825 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz
+# Copyright (C) 2021: Edouard TISSERANT
+#
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import os
+import shutil
+import hashlib
+import shlex
+import time
+
+import wx
+
+from lxml import etree
+from lxml.etree import XSLTApplyError
+
+import util.paths as paths
+from POULibrary import POULibrary
+from docutil import open_svg, get_inkscape_path
+
+from util.ProcessLogger import ProcessLogger
+from runtime.typemapping import DebugTypesSize
+import targets
+from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
+from XSLTransform import XSLTransform
+from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations,\
+ MatchTranslations, TranslationToEtree, open_pofile,\
+ GetPoFiles
+from svghmi.hmi_tree import HMI_TYPES, HMITreeNode, SPECIAL_NODES
+from svghmi.ui import SVGHMI_UI
+from svghmi.fonts import GetFontTypeAndFamilyName, GetCSSFontFaceFromFontFile
+
+
+ScriptDirectory = paths.AbsDir(__file__)
+
+
+# module scope for HMITree root
+# so that CTN can use HMITree deduced in Library
+# note: this only works because library's Generate_C is
+# systematicaly invoked before CTN's CTNGenerate_C
+
+hmi_tree_root = None
+
+on_hmitree_update = None
+
+maxConnectionsTotal = 0
+
+class SVGHMILibrary(POULibrary):
+ def GetLibraryPath(self):
+ return paths.AbsNeighbourFile(__file__, "pous.xml")
+
+ def Generate_C(self, buildpath, varlist, IECCFLAGS):
+ global hmi_tree_root, on_hmitree_update, maxConnectionsTotal
+
+ already_found_watchdog = False
+ found_SVGHMI_instance = False
+ for CTNChild in self.GetCTR().IterChildren():
+ if isinstance(CTNChild, SVGHMI):
+ found_SVGHMI_instance = True
+ # collect maximum connection total for all svghmi nodes
+ maxConnectionsTotal += CTNChild.GetParamsAttributes("SVGHMI.MaxConnections")["value"]
+
+ # spot watchdog abuse
+ if CTNChild.GetParamsAttributes("SVGHMI.EnableWatchdog")["value"]:
+ if already_found_watchdog:
+ self.FatalError("SVGHMI: Only one watchdog enabled HMI allowed")
+ already_found_watchdog = True
+
+ if not found_SVGHMI_instance:
+ self.FatalError("SVGHMI : Library is selected but not used. Please either deselect it in project config or add a SVGHMI node to project.")
+
+
+ """
+ PLC Instance Tree:
+ prog0
+ +->v1 HMI_INT
+ +->v2 HMI_INT
+ +->fb0 (type mhoo)
+ | +->va HMI_NODE
+ | +->v3 HMI_INT
+ | +->v4 HMI_INT
+ |
+ +->fb1 (type mhoo)
+ | +->va HMI_NODE
+ | +->v3 HMI_INT
+ | +->v4 HMI_INT
+ |
+ +->fb2
+ +->v5 HMI_IN
+
+ HMI tree:
+ hmi0
+ +->v1
+ +->v2
+ +->fb0 class:va
+ | +-> v3
+ | +-> v4
+ |
+ +->fb1 class:va
+ | +-> v3
+ | +-> v4
+ |
+ +->v5
+
+ """
+
+ # Filter known HMI types
+ hmi_types_instances = [v for v in varlist if v["derived"] in HMI_TYPES]
+
+ hmi_tree_root = None
+
+ # take first HMI_NODE (placed as special node), make it root
+ for i,v in enumerate(hmi_types_instances):
+ path = v["IEC_path"].split(".")
+ derived = v["derived"]
+ if derived == "HMI_NODE":
+ hmi_tree_root = HMITreeNode(path, "", derived, v["type"], v["vartype"], v["C_path"])
+ hmi_types_instances.pop(i)
+ break
+
+ # deduce HMI tree from PLC HMI_* instances
+ for v in hmi_types_instances:
+ path = v["IEC_path"].split(".")
+ # ignores variables starting with _TMP_
+ if path[-1].startswith("_TMP_"):
+ continue
+ derived = v["derived"]
+ kwargs={}
+ if derived == "HMI_NODE":
+ # TODO : make problem if HMI_NODE used in CONFIG or RESOURCE
+ name = path[-2]
+ kwargs['hmiclass'] = path[-1]
+ else:
+ name = path[-1]
+ new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], v["C_path"], **kwargs)
+ placement_result = hmi_tree_root.place_node(new_node)
+ if placement_result is not None:
+ cause, problematic_node = placement_result
+ if cause == "Non_Unique":
+ message = _("HMI tree nodes paths are not unique.\nConflicting variable: {} {}").format(
+ ".".join(problematic_node.path),
+ ".".join(new_node.path))
+
+ last_FB = None
+ for v in varlist:
+ if v["vartype"] == "FB":
+ last_FB = v
+ if v["C_path"] == problematic_node:
+ break
+ if last_FB is not None:
+ failing_parent = last_FB["type"]
+ message += "\n"
+ message += _("Solution: Add HMI_NODE at beginning of {}").format(failing_parent)
+
+ elif cause in ["Late_HMI_NODE", "Duplicate_HMI_NODE"]:
+ cause, problematic_node = placement_result
+ message = _("There must be only one occurrence of HMI_NODE before any HMI_* variable in POU.\nConflicting variable: {} {}").format(
+ ".".join(problematic_node.path),
+ ".".join(new_node.path))
+
+ self.FatalError("SVGHMI : " + message)
+
+ if on_hmitree_update is not None:
+ on_hmitree_update(hmi_tree_root)
+
+ variable_decl_array = []
+ extern_variables_declarations = []
+ buf_index = 0
+ item_count = 0
+ found_heartbeat = False
+
+ hearbeat_IEC_path = ['CONFIG', 'HEARTBEAT']
+
+ for node in hmi_tree_root.traverse():
+ if not found_heartbeat and node.path == hearbeat_IEC_path:
+ hmi_tree_hearbeat_index = item_count
+ found_heartbeat = True
+ extern_variables_declarations += [
+ "#define heartbeat_index "+str(hmi_tree_hearbeat_index)
+ ]
+ if hasattr(node, "iectype"):
+ sz = DebugTypesSize.get(node.iectype, 0)
+ variable_decl_array += [
+ "{&(" + node.cpath + "), " + node.iectype + {
+ "EXT": "_P_ENUM",
+ "IN": "_P_ENUM",
+ "MEM": "_O_ENUM",
+ "OUT": "_O_ENUM",
+ "VAR": "_ENUM"
+ }[node.vartype] + ", " +
+ str(buf_index) + ", 0, }"]
+ buf_index += sz
+ item_count += 1
+ if len(node.path) == 1:
+ extern_variables_declarations += [
+ "extern __IEC_" + node.iectype + "_" +
+ "t" if node.vartype is "VAR" else "p"
+ + node.cpath + ";"]
+
+ assert(found_heartbeat)
+
+ # TODO : filter only requiered external declarations
+ for v in varlist:
+ if v["C_path"].find('.') < 0:
+ extern_variables_declarations += [
+ "extern %(type)s %(C_path)s;" % v]
+
+ # TODO check if programs need to be declared separately
+ # "programs_declarations": "\n".join(["extern %(type)s %(C_path)s;" %
+ # p for p in self._ProgramList]),
+
+ # C code to observe/access HMI tree variables
+ svghmi_c_filepath = paths.AbsNeighbourFile(__file__, "svghmi.c")
+ svghmi_c_file = open(svghmi_c_filepath, 'r')
+ svghmi_c_code = svghmi_c_file.read()
+ svghmi_c_file.close()
+ svghmi_c_code = svghmi_c_code % {
+ "variable_decl_array": ",\n".join(variable_decl_array),
+ "extern_variables_declarations": "\n".join(extern_variables_declarations),
+ "buffer_size": buf_index,
+ "item_count": item_count,
+ "var_access_code": targets.GetCode("var_access.c"),
+ "PLC_ticktime": self.GetCTR().GetTicktime(),
+ "hmi_hash_ints": ",".join(map(str,hmi_tree_root.hash())),
+ "max_connections": maxConnectionsTotal
+ }
+
+ gen_svghmi_c_path = os.path.join(buildpath, "svghmi.c")
+ gen_svghmi_c = open(gen_svghmi_c_path, 'w')
+ gen_svghmi_c.write(svghmi_c_code)
+ gen_svghmi_c.close()
+
+ # Python based WebSocket HMITree Server
+ svghmiserverfile = open(paths.AbsNeighbourFile(__file__, "svghmi_server.py"), 'r')
+ svghmiservercode = svghmiserverfile.read()
+ svghmiserverfile.close()
+
+ runtimefile_path = os.path.join(buildpath, "runtime_00_svghmi.py")
+ runtimefile = open(runtimefile_path, 'w')
+ runtimefile.write(svghmiservercode)
+ runtimefile.close()
+
+ # Backup HMI Tree in XML form so that it can be loaded without building
+ hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
+ hmitree_backup_file = open(hmitree_backup_path, 'wb')
+ hmitree_backup_file.write(etree.tostring(hmi_tree_root.etree()))
+ hmitree_backup_file.close()
+
+ return ((["svghmi"], [(gen_svghmi_c_path, IECCFLAGS)], True), "",
+ ("runtime_00_svghmi.py", open(runtimefile_path, "rb")))
+ # ^
+ # note the double zero after "runtime_",
+ # to ensure placement before other CTN generated code in execution order
+
+ def GlobalInstances(self):
+ """ Adds HMI tree root and hearbeat to PLC Configuration's globals """
+ return [(name, iec_type, "") for name, iec_type in SPECIAL_NODES]
+
+
+
+def Register_SVGHMI_UI_for_HMI_tree_updates(ref):
+ global on_hmitree_update
+ def HMITreeUpdate(_hmi_tree_root):
+ obj = ref()
+ if obj is not None:
+ obj.HMITreeUpdate(_hmi_tree_root)
+
+ on_hmitree_update = HMITreeUpdate
+
+
+class SVGHMIEditor(ConfTreeNodeEditor):
+ CONFNODEEDITOR_TABS = [
+ (_("HMI Tree"), "CreateSVGHMI_UI")]
+
+ def CreateSVGHMI_UI(self, parent):
+ global hmi_tree_root
+
+ if hmi_tree_root is None:
+ buildpath = self.Controler.GetCTRoot()._getBuildPath()
+ hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
+ if os.path.exists(hmitree_backup_path):
+ hmitree_backup_file = open(hmitree_backup_path, 'rb')
+ hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
+
+ ret = SVGHMI_UI(parent, Register_SVGHMI_UI_for_HMI_tree_updates)
+
+ on_hmitree_update(hmi_tree_root)
+
+ return ret
+
+class SVGHMI(object):
+ XSD = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+
+ EditorType = SVGHMIEditor
+
+ ConfNodeMethods = [
+ {
+ "bitmap": "ImportSVG",
+ "name": _("Import SVG"),
+ "tooltip": _("Import SVG"),
+ "method": "_ImportSVG"
+ },
+ {
+ "bitmap": "EditSVG",
+ "name": _("Inkscape"),
+ "tooltip": _("Edit HMI"),
+ "method": "_StartInkscape"
+ },
+ {
+ "bitmap": "OpenPOT",
+ "name": _("New lang"),
+ "tooltip": _("Open non translated message catalog (POT) to start new language"),
+ "method": "_OpenPOT"
+ },
+ {
+ "bitmap": "EditPO",
+ "name": _("Edit lang"),
+ "tooltip": _("Edit existing message catalog (PO) for specific language"),
+ "method": "_EditPO"
+ },
+ {
+ "bitmap": "AddFont",
+ "name": _("Add Font"),
+ "tooltip": _("Add TTF, OTF or WOFF font to be embedded in HMI"),
+ "method": "_AddFont"
+ },
+ {
+ "bitmap": "DelFont",
+ "name": _("Delete Font"),
+ "tooltip": _("Remove font previously added to HMI"),
+ "method": "_DelFont"
+ },
+ ]
+
+ def _getSVGpath(self, project_path=None):
+ if project_path is None:
+ project_path = self.CTNPath()
+ return os.path.join(project_path, "svghmi.svg")
+
+ def _getPOTpath(self, project_path=None):
+ if project_path is None:
+ project_path = self.CTNPath()
+ return os.path.join(project_path, "messages.pot")
+
+ def OnCTNSave(self, from_project_path=None):
+ if from_project_path is not None:
+ shutil.copyfile(self._getSVGpath(from_project_path),
+ self._getSVGpath())
+ shutil.copyfile(self._getPOTpath(from_project_path),
+ self._getPOTpath())
+ # XXX TODO copy .PO files
+ return True
+
+ def GetSVGGeometry(self):
+ self.ProgressStart("inkscape", "collecting SVG geometry (Inkscape)")
+ # invoke inskscape -S, csv-parse output, produce elements
+ InkscapeGeomColumns = ["Id", "x", "y", "w", "h"]
+
+ inkpath = get_inkscape_path()
+
+ if inkpath is None:
+ self.FatalError("SVGHMI: inkscape is not installed.")
+
+ svgpath = self._getSVGpath()
+ status, result, _err_result = ProcessLogger(self.GetCTRoot().logger,
+ '"' + inkpath + '" -S "' + svgpath + '"',
+ no_stdout=True,
+ no_stderr=True).spin()
+ if status != 0:
+ self.FatalError("SVGHMI: inkscape couldn't extract geometry from given SVG.")
+
+ res = []
+ for line in result.split():
+ strippedline = line.strip()
+ attrs = dict(
+ zip(InkscapeGeomColumns, line.strip().split(',')))
+
+ res.append(etree.Element("bbox", **attrs))
+
+ self.ProgressEnd("inkscape")
+ return res
+
+ def GetHMITree(self):
+ global hmi_tree_root
+ self.ProgressStart("hmitree", "getting HMI tree")
+ res = [hmi_tree_root.etree(add_hash=True)]
+ self.ProgressEnd("hmitree")
+ return res
+
+ def GetTranslations(self, _context, msgs):
+ self.ProgressStart("i18n", "getting Translations")
+ messages = EtreeToMessages(msgs)
+
+ if len(messages) == 0:
+ self.ProgressEnd("i18n")
+ return
+
+ SaveCatalog(self._getPOTpath(), messages)
+
+ translations = ReadTranslations(self.CTNPath())
+
+ langs,translated_messages = MatchTranslations(translations, messages,
+ errcallback=self.GetCTRoot().logger.write_warning)
+
+ ret = TranslationToEtree(langs,translated_messages)
+
+ self.ProgressEnd("i18n")
+
+ return ret
+
+ def GetFontsFiles(self):
+ project_path = self.CTNPath()
+ fontdir = os.path.join(project_path, "fonts")
+ if os.path.isdir(fontdir):
+ return [os.path.join(fontdir,f) for f in sorted(os.listdir(fontdir))]
+ return []
+
+ def GetFonts(self, _context):
+ css_parts = []
+
+ for fontfile in self.GetFontsFiles():
+ if os.path.isfile(fontfile):
+ css_parts.append(GetCSSFontFaceFromFontFile(fontfile))
+
+ return "".join(css_parts)
+
+ times_msgs = {}
+ indent = 1
+ def ProgressStart(self, k, m):
+ self.times_msgs[k] = (time.time(), m)
+ self.GetCTRoot().logger.write(" "*self.indent + "Start %s...\n"%m)
+ self.indent = self.indent + 1
+
+ def ProgressEnd(self, k):
+ t = time.time()
+ oldt, m = self.times_msgs[k]
+ self.indent = self.indent - 1
+ self.GetCTRoot().logger.write(" "*self.indent + "... finished in %.3fs\n"%(t - oldt))
+
+ def get_SVGHMI_options(self):
+ name = self.BaseParams.getName()
+ port = self.GetParamsAttributes("SVGHMI.Port")["value"]
+ interface = self.GetParamsAttributes("SVGHMI.Interface")["value"]
+ path = self.GetParamsAttributes("SVGHMI.Path")["value"].format(name=name)
+ if path and path[0]=='/':
+ path = path[1:]
+ enable_watchdog = self.GetParamsAttributes("SVGHMI.EnableWatchdog")["value"]
+ url="http://"+interface+("" if port==80 else (":"+str(port))
+ ) + (("/"+path) if path else ""
+ ) + ("#watchdog" if enable_watchdog else "")
+
+ return dict(
+ name=name,
+ port=port,
+ interface=interface,
+ path=path,
+ enable_watchdog=enable_watchdog,
+ url=url)
+
+ def CTNGenerate_C(self, buildpath, locations):
+ global hmi_tree_root
+
+ if hmi_tree_root is None:
+ self.FatalError("SVGHMI : Library is not selected. Please select it in project config.")
+
+ location_str = "_".join(map(str, self.GetCurrentLocation()))
+ svghmi_options = self.get_SVGHMI_options()
+
+ svgfile = self._getSVGpath()
+
+ res = ([], "", False)
+
+ target_fname = "svghmi_"+location_str+".xhtml"
+
+ build_path = self._getBuildPath()
+ target_path = os.path.join(build_path, target_fname)
+ hash_path = os.path.join(build_path, "svghmi.md5")
+
+ self.GetCTRoot().logger.write("SVGHMI:\n")
+
+ if os.path.exists(svgfile):
+
+ hasher = hashlib.md5()
+ hmi_tree_root._hash(hasher)
+ pofiles = GetPoFiles(self.CTNPath())
+ filestocheck = [svgfile] + \
+ (list(zip(*pofiles)[1]) if pofiles else []) + \
+ self.GetFontsFiles()
+
+ for filetocheck in filestocheck:
+ with open(filetocheck, 'rb') as afile:
+ while True:
+ buf = afile.read(65536)
+ if len(buf) > 0:
+ hasher.update(buf)
+ else:
+ break
+ digest = hasher.hexdigest()
+
+ if os.path.exists(hash_path):
+ with open(hash_path, 'rb') as digest_file:
+ last_digest = digest_file.read()
+ else:
+ last_digest = None
+
+ if digest != last_digest:
+
+ transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"),
+ [("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()),
+ ("GetHMITree", lambda *_ignored:self.GetHMITree()),
+ ("GetTranslations", self.GetTranslations),
+ ("GetFonts", self.GetFonts),
+ ("ProgressStart", lambda _ign,k,m:self.ProgressStart(str(k),str(m))),
+ ("ProgressEnd", lambda _ign,k:self.ProgressEnd(str(k)))])
+
+ self.ProgressStart("svg", "source SVG parsing")
+
+ # load svg as a DOM with Etree
+ svgdom = etree.parse(svgfile)
+
+ self.ProgressEnd("svg")
+
+ # call xslt transform on Inkscape's SVG to generate XHTML
+ try:
+ self.ProgressStart("xslt", "XSLT transform")
+ result = transform.transform(svgdom) # , profile_run=True)
+ self.ProgressEnd("xslt")
+ except XSLTApplyError as e:
+ self.FatalError("SVGHMI " + svghmi_options["name"] + ": " + e.message)
+ finally:
+ for entry in transform.get_error_log():
+ message = "SVGHMI: "+ entry.message + "\n"
+ self.GetCTRoot().logger.write_warning(message)
+
+ target_file = open(target_path, 'wb')
+ result.write(target_file, encoding="utf-8")
+ target_file.close()
+
+ # print(str(result))
+ # print(transform.xslt.error_log)
+ # print(etree.tostring(result.xslt_profile,pretty_print=True))
+
+ with open(hash_path, 'wb') as digest_file:
+ digest_file.write(digest)
+ else:
+ self.GetCTRoot().logger.write(" No changes - XSLT transformation skipped\n")
+
+ else:
+ target_file = open(target_path, 'wb')
+ target_file.write("""
+
+
+SVGHMI
+
+
+ No SVG file provided
+
+
+""")
+ target_file.close()
+
+ res += ((target_fname, open(target_path, "rb")),)
+
+ svghmi_cmds = {}
+ for thing in ["Start", "Stop", "Watchdog"]:
+ given_command = self.GetParamsAttributes("SVGHMI.On"+thing)["value"]
+ svghmi_cmds[thing] = (
+ "Popen(" +
+ repr(shlex.split(given_command.format(**svghmi_options))) +
+ ")") if given_command else "pass # no command given"
+
+ runtimefile_path = os.path.join(buildpath, "runtime_%s_svghmi_.py" % location_str)
+ runtimefile = open(runtimefile_path, 'w')
+ runtimefile.write("""
+# TODO : multiple watchdog (one for each svghmi instance)
+def svghmi_{location}_watchdog_trigger():
+ {svghmi_cmds[Watchdog]}
+
+max_svghmi_sessions = {maxConnections_total}
+
+def _runtime_{location}_svghmi_start():
+ global svghmi_watchdog, svghmi_servers
+
+ srv = svghmi_servers.get("{interface}:{port}", None)
+ if srv is not None:
+ svghmi_root, svghmi_listener, path_list = srv
+ if '{path}' in path_list:
+ raise Exception("SVGHMI {name}: path {path} already used on {interface}:{port}")
+ else:
+ svghmi_root = Resource()
+ factory = HMIWebSocketServerFactory()
+ factory.setProtocolOptions(maxConnections={maxConnections})
+
+ svghmi_root.putChild("ws", WebSocketResource(factory))
+
+ svghmi_listener = reactor.listenTCP({port}, Site(svghmi_root), interface='{interface}')
+ path_list = []
+ svghmi_servers["{interface}:{port}"] = (svghmi_root, svghmi_listener, path_list)
+
+ svghmi_root.putChild(
+ '{path}',
+ NoCacheFile('{xhtml}',
+ defaultType='application/xhtml+xml'))
+
+ path_list.append("{path}")
+
+ {svghmi_cmds[Start]}
+
+ if {enable_watchdog}:
+ if svghmi_watchdog is None:
+ svghmi_watchdog = Watchdog(
+ {watchdog_initial},
+ {watchdog_interval},
+ svghmi_{location}_watchdog_trigger)
+ else:
+ raise Exception("SVGHMI {name}: only one watchdog allowed")
+
+
+def _runtime_{location}_svghmi_stop():
+ global svghmi_watchdog, svghmi_servers
+
+ if svghmi_watchdog is not None:
+ svghmi_watchdog.cancel()
+ svghmi_watchdog = None
+
+ svghmi_root, svghmi_listener, path_list = svghmi_servers["{interface}:{port}"]
+ svghmi_root.delEntity('{path}')
+
+ path_list.remove('{path}')
+
+ if len(path_list)==0:
+ svghmi_root.delEntity("ws")
+ svghmi_listener.stopListening()
+ svghmi_servers.pop("{interface}:{port}")
+
+ {svghmi_cmds[Stop]}
+
+ """.format(location=location_str,
+ xhtml=target_fname,
+ svghmi_cmds=svghmi_cmds,
+ watchdog_initial = self.GetParamsAttributes("SVGHMI.WatchdogInitial")["value"],
+ watchdog_interval = self.GetParamsAttributes("SVGHMI.WatchdogInterval")["value"],
+ maxConnections = self.GetParamsAttributes("SVGHMI.MaxConnections")["value"],
+ maxConnections_total = maxConnectionsTotal,
+ **svghmi_options
+ ))
+
+ runtimefile.close()
+
+ res += (("runtime_%s_svghmi.py" % location_str, open(runtimefile_path, "rb")),)
+
+ return res
+
+ def _ImportSVG(self):
+ dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a SVG file"), os.getcwd(), "", _("SVG files (*.svg)|*.svg|All files|*.*"), wx.OPEN)
+ if dialog.ShowModal() == wx.ID_OK:
+ svgpath = dialog.GetPath()
+ if os.path.isfile(svgpath):
+ shutil.copy(svgpath, self._getSVGpath())
+ else:
+ self.GetCTRoot().logger.write_error(_("No such SVG file: %s\n") % svgpath)
+ dialog.Destroy()
+
+ def getDefaultSVG(self):
+ return os.path.join(ScriptDirectory, "default.svg")
+
+ def _StartInkscape(self):
+ svgfile = self._getSVGpath()
+ open_inkscape = True
+ if not self.GetCTRoot().CheckProjectPathPerm():
+ dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
+ _("You don't have write permissions.\nOpen Inkscape anyway ?"),
+ _("Open Inkscape"),
+ wx.YES_NO | wx.ICON_QUESTION)
+ open_inkscape = dialog.ShowModal() == wx.ID_YES
+ dialog.Destroy()
+ if open_inkscape:
+ if not os.path.isfile(svgfile):
+ # make a copy of default svg from source
+ default = self.getDefaultSVG()
+ shutil.copyfile(default, svgfile)
+ open_svg(svgfile)
+
+ def _StartPOEdit(self, POFile):
+ open_poedit = True
+ if not self.GetCTRoot().CheckProjectPathPerm():
+ dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
+ _("You don't have write permissions.\nOpen POEdit anyway ?"),
+ _("Open POEdit"),
+ wx.YES_NO | wx.ICON_QUESTION)
+ open_poedit = dialog.ShowModal() == wx.ID_YES
+ dialog.Destroy()
+ if open_poedit:
+ open_pofile(POFile)
+
+ def _EditPO(self):
+ """ Select a specific translation and edit it with POEdit """
+ project_path = self.CTNPath()
+ dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a PO file"), project_path, "", _("PO files (*.po)|*.po"), wx.OPEN)
+ if dialog.ShowModal() == wx.ID_OK:
+ POFile = dialog.GetPath()
+ if os.path.isfile(POFile):
+ if os.path.relpath(POFile, project_path) == os.path.basename(POFile):
+ self._StartPOEdit(POFile)
+ else:
+ self.GetCTRoot().logger.write_error(_("PO file misplaced: %s is not in %s\n") % (POFile,project_path))
+ else:
+ self.GetCTRoot().logger.write_error(_("PO file does not exist: %s\n") % POFile)
+ dialog.Destroy()
+
+ def _OpenPOT(self):
+ """ Start POEdit with untouched empty catalog """
+ POFile = self._getPOTpath()
+ if os.path.isfile(POFile):
+ self._StartPOEdit(POFile)
+ else:
+ self.GetCTRoot().logger.write_error(_("POT file does not exist, add translatable text (label starting with '_') in Inkscape first\n"))
+
+ def _AddFont(self):
+ dialog = wx.FileDialog(
+ self.GetCTRoot().AppFrame,
+ _("Choose a font"),
+ os.path.expanduser("~"),
+ "",
+ _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.OPEN)
+
+ if dialog.ShowModal() == wx.ID_OK:
+ fontfile = dialog.GetPath()
+ if os.path.isfile(fontfile):
+ familyname, uniquename, formatname, mimetype = GetFontTypeAndFamilyName(fontfile)
+ else:
+ self.GetCTRoot().logger.write_error(
+ _('Selected font %s is not a readable file\n')%fontfile)
+ return
+ if familyname is None or uniquename is None or formatname is None or mimetype is None:
+ self.GetCTRoot().logger.write_error(
+ _('Selected font file %s is invalid or incompatible\n')%fontfile)
+ return
+
+ project_path = self.CTNPath()
+
+ fontfname = uniquename + "." + mimetype.split('/')[1]
+ fontdir = os.path.join(project_path, "fonts")
+ newfontfile = os.path.join(fontdir, fontfname)
+
+ if not os.path.exists(fontdir):
+ os.mkdir(fontdir)
+
+ shutil.copyfile(fontfile, newfontfile)
+
+ self.GetCTRoot().logger.write(
+ _('Added font %s as %s\n')%(fontfile,newfontfile))
+
+ def _DelFont(self):
+ project_path = self.CTNPath()
+ fontdir = os.path.join(project_path, "fonts")
+ dialog = wx.FileDialog(
+ self.GetCTRoot().AppFrame,
+ _("Choose a font to remove"),
+ fontdir,
+ "",
+ _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.OPEN)
+ if dialog.ShowModal() == wx.ID_OK:
+ fontfile = dialog.GetPath()
+ if os.path.isfile(fontfile):
+ if os.path.relpath(fontfile, fontdir) == os.path.basename(fontfile):
+ os.remove(fontfile)
+ self.GetCTRoot().logger.write(
+ _('Removed font %s\n')%fontfile)
+ else:
+ self.GetCTRoot().logger.write_error(
+ _("Font to remove %s is not in %s\n") % (fontfile,fontdir))
+ else:
+ self.GetCTRoot().logger.write_error(
+ _("Font file does not exist: %s\n") % fontfile)
+
+ ## In case one day we support more than one heartbeat
+ # def CTNGlobalInstances(self):
+ # view_name = self.BaseParams.getName()
+ # return [(view_name + "_HEARTBEAT", "HMI_INT", "")]
+
+ def GetIconName(self):
+ return "SVGHMI"