andrej@1511: #!/usr/bin/env python
andrej@1511: # -*- coding: utf-8 -*-
andrej@1511: 
andrej@1511: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1511: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
andrej@1511: #
andrej@1511: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
andrej@1511: #
andrej@1511: # See COPYING file for copyrights details.
andrej@1511: #
andrej@1511: # This program is free software; you can redistribute it and/or
andrej@1511: # modify it under the terms of the GNU General Public License
andrej@1511: # as published by the Free Software Foundation; either version 2
andrej@1511: # of the License, or (at your option) any later version.
andrej@1511: #
andrej@1511: # This program is distributed in the hope that it will be useful,
andrej@1511: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1511: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1511: # GNU General Public License for more details.
andrej@1511: #
andrej@1511: # You should have received a copy of the GNU General Public License
andrej@1511: # along with this program; if not, write to the Free Software
andrej@1511: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
andrej@1511: 
etisserant@14: """
Edouard@725: Config Tree Node base class.
Edouard@725: 
Edouard@725: - A Beremiz project is organized in a tree each node derivate from ConfigTreeNode
Edouard@725: - Project tree organization match filesystem organization of project directory.
Edouard@725: - Each node of the tree have its own xml configuration, whose grammar is defined for each node type, as XSD
Edouard@725: - ... TODO : document
etisserant@14: """
etisserant@14: 
kinsamanka@3750: 
andrej@1732: import os
andrej@1732: import traceback
andrej@1732: import types
etisserant@14: import shutil
Edouard@2523: from operator import add
Edouard@2551: from functools import reduce
andrej@2434: 
Laurent@1315: from lxml import etree
Laurent@1315: 
Laurent@1315: from xmlclass import GenerateParserFromXSDstring
andrej@1850: from PLCControler import LOCATION_CONFNODE
Laurent@814: from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
edouard@2711: from POULibrary import UserAddressedException
greg@274: 
Laurent@1315: _BaseParamsParser = GenerateParserFromXSDstring("""<?xml version="1.0" encoding="ISO-8859-1" ?>
etisserant@14:         <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
etisserant@14:           <xsd:element name="BaseParams">
etisserant@14:             <xsd:complexType>
lbessard@86:               <xsd:attribute name="Name" type="xsd:string" use="optional" default="__unnamed__"/>
lbessard@17:               <xsd:attribute name="IEC_Channel" type="xsd:integer" use="required"/>
lbessard@86:               <xsd:attribute name="Enabled" type="xsd:boolean" use="optional" default="true"/>
etisserant@14:             </xsd:complexType>
etisserant@14:           </xsd:element>
Laurent@1315:         </xsd:schema>""")
etisserant@14: 
etisserant@14: NameTypeSeparator = '@'
andrej@1722: XSDSchemaErrorMessage = _("{a1} XML file doesn't follow XSD schema at line {a2}:\n{a3}")
etisserant@14: 
andrej@1736: 
andrej@1831: class ConfigTreeNode(object):
etisserant@14:     """
Edouard@717:     This class is the one that define confnodes.
etisserant@14:     """
etisserant@14: 
etisserant@14:     XSD = None
Edouard@718:     CTNChildrenTypes = []
Edouard@718:     CTNMaxCount = None
Edouard@717:     ConfNodeMethods = []
greg@274:     LibraryControler = None
laurent@743:     EditorType = ConfTreeNodeEditor
laurent@738:     IconPath = None
andrej@1730: 
etisserant@14:     def _AddParamsMembers(self):
Edouard@718:         self.CTNParams = None
etisserant@29:         if self.XSD:
Laurent@1315:             self.Parser = GenerateParserFromXSDstring(self.XSD)
Laurent@1315:             obj = self.Parser.CreateRoot()
Laurent@1315:             name = obj.getLocalTag()
Laurent@1315:             self.CTNParams = (name, obj)
Laurent@1315:             setattr(self, name, obj)
lbessard@17: 
lbessard@17:     def __init__(self):
andrej@1730:         # Create BaseParam
Laurent@1315:         self.BaseParams = _BaseParamsParser.CreateRoot()
lbessard@17:         self.MandatoryParams = ("BaseParams", self.BaseParams)
etisserant@14:         self._AddParamsMembers()
Edouard@718:         self.Children = {}
laurent@656:         self._View = None
Edouard@717:         # copy ConfNodeMethods so that it can be later customized
Edouard@717:         self.ConfNodeMethods = [dic.copy() for dic in self.ConfNodeMethods]
andrej@1730: 
Edouard@718:     def ConfNodeBaseXmlFilePath(self, CTNName=None):
Edouard@718:         return os.path.join(self.CTNPath(CTNName), "baseconfnode.xml")
andrej@1730: 
Edouard@718:     def ConfNodeXmlFilePath(self, CTNName=None):
Edouard@718:         return os.path.join(self.CTNPath(CTNName), "confnode.xml")
Edouard@717: 
andrej@1740:     def CTNPath(self, CTNName=None, project_path=None):
Edouard@718:         if not CTNName:
Edouard@718:             CTNName = self.CTNName()
Laurent@1061:         if not project_path:
Laurent@1061:             project_path = self.CTNParent.CTNPath()
Laurent@1061:         return os.path.join(project_path,
Edouard@718:                             CTNName + NameTypeSeparator + self.CTNType)
andrej@1730: 
Edouard@718:     def CTNName(self):
laurent@675:         return self.BaseParams.getName()
andrej@1730: 
Edouard@718:     def CTNEnabled(self):
laurent@675:         return self.BaseParams.getEnabled()
andrej@1730: 
Edouard@718:     def CTNFullName(self):
Edouard@718:         parent = self.CTNParent.CTNFullName()
laurent@656:         if parent != "":
Edouard@718:             return parent + "." + self.CTNName()
laurent@656:         return self.BaseParams.getName()
andrej@1730: 
Edouard@2523:     def CTNSearch(self, criteria):
Edouard@2523:         # TODO match config's fields name and fields contents
Edouard@2523:         return reduce(add, [
Edouard@2523:             CTNChild.CTNSearch(criteria)
edouard@2579:             for CTNChild in self.IterChildren()], [])
Edouard@2523: 
laurent@781:     def GetIconName(self):
Edouard@734:         return None
andrej@1730: 
Edouard@718:     def CTNTestModified(self):
etisserant@118:         return self.ChangesToSave
etisserant@118: 
edouard@3674:     def CTNMarkModified(self):
edouard@3674:         oldChangesToSave = self.ChangesToSave
edouard@3674:         self.ChangesToSave = True
edouard@3674:         if not oldChangesToSave:
edouard@3674:             appframe = self.GetCTRoot().AppFrame
edouard@3674:             if appframe is not None:
edouard@3674:                 appframe.RefreshTitle()
edouard@3674:                 appframe.RefreshPageTitles()
edouard@4000:                 appframe.RefreshFileMenu()
edouard@3674: 
etisserant@118:     def ProjectTestModified(self):
etisserant@118:         """
etisserant@118:         recursively check modified status
etisserant@118:         """
Edouard@718:         if self.CTNTestModified():
etisserant@118:             return True
etisserant@118: 
Edouard@718:         for CTNChild in self.IterChildren():
Edouard@718:             if CTNChild.ProjectTestModified():
etisserant@118:                 return True
etisserant@118: 
etisserant@14:         return False
andrej@1730: 
laurent@699:     def RemoteExec(self, script, **kwargs):
Edouard@718:         return self.CTNParent.RemoteExec(script, **kwargs)
andrej@1730: 
Laurent@1061:     def OnCTNSave(self, from_project_path=None):
andrej@1782:         """Default, do nothing and return success"""
etisserant@14:         return True
etisserant@14: 
andrej@1744:     def GetParamsAttributes(self, path=None):
lbessard@19:         if path:
lbessard@19:             parts = path.split(".", 1)
lbessard@19:             if self.MandatoryParams and parts[0] == self.MandatoryParams[0]:
lbessard@19:                 return self.MandatoryParams[1].getElementInfos(parts[0], parts[1])
Edouard@718:             elif self.CTNParams and parts[0] == self.CTNParams[0]:
Edouard@718:                 return self.CTNParams[1].getElementInfos(parts[0], parts[1])
lbessard@17:         else:
lbessard@19:             params = []
Edouard@718:             if self.CTNParams:
Edouard@718:                 params.append(self.CTNParams[1].getElementInfos(self.CTNParams[0]))
lbessard@19:             return params
andrej@1730: 
etisserant@203:     def SetParamsAttribute(self, path, value):
etisserant@118:         self.ChangesToSave = True
etisserant@29:         # Filter IEC_Channel and Name, that have specific behavior
etisserant@29:         if path == "BaseParams.IEC_Channel":
laurent@443:             old_leading = ".".join(map(str, self.GetCurrentLocation()))
laurent@443:             new_value = self.FindNewIEC_Channel(value)
Laurent@842:             if new_value != value:
Laurent@842:                 new_leading = ".".join(map(str, self.CTNParent.GetCurrentLocation() + (new_value,)))
Laurent@842:                 self.GetCTRoot().UpdateProjectVariableLocation(old_leading, new_leading)
laurent@443:             return new_value, True
etisserant@29:         elif path == "BaseParams.Name":
etisserant@203:             res = self.FindNewName(value)
Edouard@718:             self.CTNRequestSave()
etisserant@118:             return res, True
andrej@1730: 
lbessard@19:         parts = path.split(".", 1)
lbessard@19:         if self.MandatoryParams and parts[0] == self.MandatoryParams[0]:
lbessard@19:             self.MandatoryParams[1].setElementValue(parts[1], value)
Laurent@1179:             value = self.MandatoryParams[1].getElementInfos(parts[0], parts[1])["value"]
Edouard@718:         elif self.CTNParams and parts[0] == self.CTNParams[0]:
Edouard@718:             self.CTNParams[1].setElementValue(parts[1], value)
Laurent@1179:             value = self.CTNParams[1].getElementInfos(parts[0], parts[1])["value"]
etisserant@29:         return value, False
lbessard@17: 
Edouard@718:     def CTNMakeDir(self):
Edouard@718:         os.mkdir(self.CTNPath())
Edouard@718: 
Laurent@1061:     def CTNRequestSave(self, from_project_path=None):
edouard@3318:         if self.GetCTRoot().CheckProjectPathPerm():
Edouard@717:             # If confnode do not have corresponding directory
Edouard@718:             ctnpath = self.CTNPath()
Edouard@718:             if not os.path.isdir(ctnpath):
greg@427:                 # Create it
Edouard@718:                 os.mkdir(ctnpath)
andrej@1730: 
Edouard@717:             # generate XML for base XML parameters controller of the confnode
greg@427:             if self.MandatoryParams:
kinsamanka@3755:                 BaseXMLFile = open(self.ConfNodeBaseXmlFilePath(), 'w', encoding='utf-8')
Laurent@1315:                 BaseXMLFile.write(etree.tostring(
andrej@1730:                     self.MandatoryParams[1],
andrej@1730:                     pretty_print=True,
andrej@1730:                     xml_declaration=True,
kinsamanka@3755:                     encoding='utf-8').decode())
greg@427:                 BaseXMLFile.close()
andrej@1730: 
Edouard@717:             # generate XML for XML parameters controller of the confnode
Edouard@718:             if self.CTNParams:
kinsamanka@3755:                 XMLFile = open(self.ConfNodeXmlFilePath(), 'w', encoding='utf-8')
Laurent@1315:                 XMLFile.write(etree.tostring(
andrej@1730:                     self.CTNParams[1],
andrej@1730:                     pretty_print=True,
andrej@1730:                     xml_declaration=True,
kinsamanka@3755:                     encoding='utf-8').decode())
greg@427:                 XMLFile.close()
andrej@1730: 
Edouard@718:             # Call the confnode specific OnCTNSave method
Laurent@1061:             result = self.OnCTNSave(from_project_path)
greg@427:             if not result:
andrej@1734:                 return _("Error while saving \"%s\"\n") % self.CTNPath()
andrej@1730: 
Edouard@717:             # mark confnode as saved
greg@427:             self.ChangesToSave = False
Edouard@718:             # go through all children and do the same
Edouard@718:             for CTNChild in self.IterChildren():
Laurent@1063:                 CTNChildPath = None
Laurent@1063:                 if from_project_path is not None:
Laurent@1063:                     CTNChildPath = CTNChild.CTNPath(project_path=from_project_path)
Laurent@1063:                 result = CTNChild.CTNRequestSave(CTNChildPath)
greg@427:                 if result:
greg@427:                     return result
lbessard@17:         return None
andrej@1730: 
Edouard@718:     def CTNImport(self, src_CTNPath):
Edouard@718:         shutil.copytree(src_CTNPath, self.CTNPath)
etisserant@14:         return True
etisserant@14: 
Laurent@883:     def CTNGlobalInstances(self):
Laurent@883:         """
Laurent@883:         @return: [(instance_name, instance_type),...]
Laurent@883:         """
Laurent@883:         return []
andrej@1730: 
Laurent@883:     def _GlobalInstances(self):
Laurent@883:         instances = self.CTNGlobalInstances()
Laurent@883:         for CTNChild in self.IECSortedChildren():
Laurent@883:             instances.extend(CTNChild._GlobalInstances())
Laurent@883:         return instances
andrej@1730: 
Edouard@718:     def CTNGenerate_C(self, buildpath, locations):
etisserant@14:         """
etisserant@14:         Generate C code
etisserant@14:         @param locations: List of complete variables locations \
etisserant@22:             [{"IEC_TYPE" : the IEC type (i.e. "INT", "STRING", ...)
etisserant@22:             "NAME" : name of the variable (generally "__IW0_1_2" style)
etisserant@22:             "DIR" : direction "Q","I" or "M"
etisserant@22:             "SIZE" : size "X", "B", "W", "D", "L"
etisserant@22:             "LOC" : tuple of interger for IEC location (0,1,2,...)
etisserant@22:             }, ...]
etisserant@18:         @return: [(C_file_name, CFLAGS),...] , LDFLAGS_TO_APPEND
etisserant@18:         """
andrej@1833:         self.GetCTRoot().logger.write_warning(".".join(map(str, self.GetCurrentLocation())) + " -> Nothing to do\n")
andrej@1740:         return [], "", False
andrej@1730: 
etisserant@203:     def _Generate_C(self, buildpath, locations):
Edouard@717:         # Generate confnodes [(Cfiles, CFLAGS)], LDFLAGS, DoCalls, extra_files
etisserant@203:         # extra_files = [(fname,fobject), ...]
Edouard@718:         gen_result = self.CTNGenerate_C(buildpath, locations)
Edouard@718:         CTNCFilesAndCFLAGS, CTNLDFLAGS, DoCalls = gen_result[:3]
etisserant@203:         extra_files = gen_result[3:]
laurent@361:         # if some files have been generated put them in the list with their location
Edouard@718:         if CTNCFilesAndCFLAGS:
Edouard@718:             LocationCFilesAndCFLAGS = [(self.GetCurrentLocation(), CTNCFilesAndCFLAGS, DoCalls)]
etisserant@47:         else:
etisserant@47:             LocationCFilesAndCFLAGS = []
etisserant@47: 
Edouard@717:         # confnode asks for some LDFLAGS
Edouard@2003:         LDFLAGS = []
Edouard@2003:         if CTNLDFLAGS is not None:
etisserant@47:             # LDFLAGS can be either string
kinsamanka@3752:             if isinstance(CTNLDFLAGS, str):
Edouard@2003:                 LDFLAGS += [CTNLDFLAGS]
andrej@1733:             # or list of strings
andrej@1778:             elif isinstance(CTNLDFLAGS, list):
Edouard@2003:                 LDFLAGS += CTNLDFLAGS
andrej@1730: 
Edouard@718:         # recurse through all children, and stack their results
Edouard@718:         for CTNChild in self.IECSortedChildren():
Edouard@718:             new_location = CTNChild.GetCurrentLocation()
etisserant@24:             # How deep are we in the tree ?
andrej@1742:             depth = len(new_location)
etisserant@203:             _LocationCFilesAndCFLAGS, _LDFLAGS, _extra_files = \
Edouard@718:                 CTNChild._Generate_C(
andrej@1733:                     # keep the same path
etisserant@14:                     buildpath,
etisserant@14:                     # filter locations that start with current IEC location
andrej@1746:                     [loc for loc in locations if loc["LOC"][0:depth] == new_location])
etisserant@14:             # stack the result
etisserant@47:             LocationCFilesAndCFLAGS += _LocationCFilesAndCFLAGS
etisserant@47:             LDFLAGS += _LDFLAGS
etisserant@203:             extra_files += _extra_files
andrej@1730: 
etisserant@203:         return LocationCFilesAndCFLAGS, LDFLAGS, extra_files
etisserant@14: 
Edouard@718:     def IterChildren(self):
kinsamanka@3750:         for _CTNType, Children in list(self.Children.items()):
Edouard@718:             for CTNInstance in Children:
Edouard@718:                 yield CTNInstance
andrej@1730: 
Edouard@718:     def IECSortedChildren(self):
Edouard@718:         # reorder children by IEC_channels
andrej@1740:         ordered = [(chld.BaseParams.getIEC_Channel(), chld) for chld in self.IterChildren()]
etisserant@47:         if ordered:
etisserant@47:             ordered.sort()
kinsamanka@3750:             return list(zip(*ordered))[1]
etisserant@47:         else:
etisserant@47:             return []
andrej@1730: 
etisserant@47:     def _GetChildBySomething(self, something, toks):
Edouard@718:         for CTNInstance in self.IterChildren():
etisserant@14:             # if match component of the name
Edouard@718:             if getattr(CTNInstance.BaseParams, something) == toks[0]:
etisserant@14:                 # if Name have other components
etisserant@47:                 if len(toks) >= 2:
etisserant@14:                     # Recurse in order to find the latest object
andrej@1747:                     return CTNInstance._GetChildBySomething(something, toks[1:])
etisserant@14:                 # No sub name -> found
Edouard@718:                 return CTNInstance
etisserant@14:         # Not found
etisserant@14:         return None
etisserant@14: 
etisserant@14:     def GetChildByName(self, Name):
etisserant@47:         if Name:
etisserant@47:             toks = Name.split('.')
etisserant@47:             return self._GetChildBySomething("Name", toks)
etisserant@47:         else:
etisserant@47:             return self
etisserant@14: 
etisserant@14:     def GetChildByIECLocation(self, Location):
etisserant@47:         if Location:
etisserant@47:             return self._GetChildBySomething("IEC_Channel", Location)
etisserant@47:         else:
etisserant@47:             return self
andrej@1730: 
etisserant@23:     def GetCurrentLocation(self):
etisserant@24:         """
Edouard@717:         @return:  Tupple containing confnode IEC location of current confnode : %I0.0.4.5 => (0,0,4,5)
etisserant@24:         """
Edouard@718:         return self.CTNParent.GetCurrentLocation() + (self.BaseParams.getIEC_Channel(),)
etisserant@23: 
etisserant@47:     def GetCurrentName(self):
etisserant@47:         """
etisserant@47:         @return:  String "ParentParentName.ParentName.Name"
etisserant@47:         """
andrej@1738:         return self.CTNParent._GetCurrentName() + self.BaseParams.getName()
etisserant@47: 
etisserant@47:     def _GetCurrentName(self):
etisserant@47:         """
etisserant@47:         @return:  String "ParentParentName.ParentName.Name."
etisserant@47:         """
andrej@1738:         return self.CTNParent._GetCurrentName() + self.BaseParams.getName() + "."
Edouard@718: 
Edouard@718:     def GetCTRoot(self):
Edouard@718:         return self.CTNParent.GetCTRoot()
etisserant@23: 
lbessard@97:     def GetFullIEC_Channel(self):
lbessard@97:         return ".".join([str(i) for i in self.GetCurrentLocation()]) + ".x"
lbessard@97: 
lbessard@97:     def GetLocations(self):
lbessard@97:         location = self.GetCurrentLocation()
Edouard@718:         return [loc for loc in self.CTNParent.GetLocations() if loc["LOC"][0:len(location)] == location]
lbessard@97: 
laurent@401:     def GetVariableLocationTree(self):
laurent@401:         '''
Edouard@717:         This function is meant to be overridden by confnodes.
laurent@401: 
laurent@401:         It should returns an list of dictionaries
andrej@1730: 
laurent@401:         - IEC_type is an IEC type like BOOL/BYTE/SINT/...
laurent@401:         - location is a string of this variable's location, like "%IX0.0.0"
laurent@401:         '''
laurent@401:         children = []
Edouard@718:         for child in self.IECSortedChildren():
laurent@402:             children.append(child.GetVariableLocationTree())
laurent@402:         return {"name": self.BaseParams.getName(),
Edouard@717:                 "type": LOCATION_CONFNODE,
laurent@402:                 "location": self.GetFullIEC_Channel(),
laurent@402:                 "children": children}
laurent@401: 
etisserant@203:     def FindNewName(self, DesiredName):
etisserant@29:         """
etisserant@29:         Changes Name to DesiredName if available, Name-N if not.
etisserant@29:         @param DesiredName: The desired Name (string)
etisserant@29:         """
andrej@1846: 
Edouard@718:         # Build a list of used Name out of parent's Children
andrej@1742:         AllNames = []
Edouard@718:         for CTNInstance in self.CTNParent.IterChildren():
Edouard@718:             if CTNInstance != self:
Edouard@718:                 AllNames.append(CTNInstance.BaseParams.getName())
etisserant@29: 
etisserant@29:         # Find a free name, eventually appending digit
etisserant@29:         res = DesiredName
laurent@833:         if DesiredName.endswith("_0"):
Laurent@841:             BaseDesiredName = DesiredName[:-2]
Laurent@841:         else:
Laurent@841:             BaseDesiredName = DesiredName
etisserant@29:         suffix = 1
etisserant@29:         while res in AllNames:
andrej@1734:             res = "%s_%d" % (BaseDesiredName, suffix)
etisserant@29:             suffix += 1
andrej@1730: 
Edouard@717:         # Check previous confnode existance
etisserant@29:         dontexist = self.BaseParams.getName() == "__unnamed__"
Edouard@1932:         if not dontexist:
Edouard@1932:             # Get old path
Edouard@1932:             oldpath = self.CTNPath()
etisserant@29:         # Set the new name
etisserant@29:         self.BaseParams.setName(res)
Edouard@717:         # Rename confnode dir if exist
etisserant@29:         if not dontexist:
Edouard@1932:             shutil.move(oldpath, self.CTNPath())
etisserant@29:         # warn user he has two left hands
etisserant@29:         if DesiredName != res:
andrej@1744:             msg = _("A child named \"{a1}\" already exists -> \"{a2}\"\n").format(a1=DesiredName, a2=res)
andrej@1581:             self.GetCTRoot().logger.write_warning(msg)
etisserant@29:         return res
etisserant@29: 
laurent@683:     def GetAllChannels(self):
andrej@1742:         AllChannels = []
Edouard@718:         for CTNInstance in self.CTNParent.IterChildren():
Edouard@718:             if CTNInstance != self:
Edouard@718:                 AllChannels.append(CTNInstance.BaseParams.getIEC_Channel())
laurent@683:         AllChannels.sort()
laurent@683:         return AllChannels
laurent@683: 
etisserant@203:     def FindNewIEC_Channel(self, DesiredChannel):
etisserant@14:         """
etisserant@14:         Changes IEC Channel number to DesiredChannel if available, nearest available if not.
etisserant@14:         @param DesiredChannel: The desired IEC channel (int)
etisserant@14:         """
etisserant@14:         # Get Current IEC channel
etisserant@14:         CurrentChannel = self.BaseParams.getIEC_Channel()
etisserant@14:         # Do nothing if no change
andrej@1782:         # if CurrentChannel == DesiredChannel: return CurrentChannel
Edouard@718:         # Build a list of used Channels out of parent's Children
laurent@683:         AllChannels = self.GetAllChannels()
andrej@1730: 
etisserant@14:         # Now, try to guess the nearest available channel
etisserant@14:         res = DesiredChannel
andrej@1737:         while res in AllChannels:  # While channel not free
andrej@1737:             if res < CurrentChannel:  # Want to go down ?
andrej@1758:                 res -= 1  # Test for n-1
andrej@1739:                 if res < 0:
andrej@1734:                     self.GetCTRoot().logger.write_warning(_("Cannot find lower free IEC channel than %d\n") % CurrentChannel)
andrej@1737:                     return CurrentChannel  # Can't go bellow 0, do nothing
andrej@1739:             else:  # Want to go up ?
andrej@1758:                 res += 1  # Test for n-1
etisserant@14:         # Finally set IEC Channel
etisserant@14:         self.BaseParams.setIEC_Channel(res)
etisserant@14:         return res
etisserant@14: 
Laurent@967:     def GetContextualMenuItems(self):
Laurent@967:         return None
Laurent@967: 
edouard@2696:     def GetView(self, onlyopened=False):
edouard@3641:         if not self._View and not onlyopened and self.EditorType is not None:
Edouard@2524:             app_frame = self.GetCTRoot().AppFrame
Edouard@2524:             self._View = self.EditorType(app_frame.TabsOpened, self, app_frame)
Edouard@2524: 
Edouard@2524:         return self._View
Edouard@2524: 
laurent@782:     def _OpenView(self, name=None, onlyopened=False):
edouard@2696:         view = self.GetView(onlyopened)
Edouard@2524: 
Edouard@2524:         if view is not None:
Edouard@2524:             if name is None:
Edouard@2524:                 name = self.CTNFullName()
laurent@782:             app_frame = self.GetCTRoot().AppFrame
edouard@2695:             app_frame.EditProjectElement(view, name, onlyopened)
Edouard@2524: 
Edouard@2524:         return view
laurent@675: 
laurent@774:     def _CloseView(self, view):
laurent@774:         app_frame = self.GetCTRoot().AppFrame
laurent@774:         if app_frame is not None:
laurent@774:             app_frame.DeletePage(view)
laurent@774: 
laurent@675:     def OnCloseEditor(self, view):
laurent@675:         if self._View == view:
laurent@675:             self._View = None
laurent@656: 
Edouard@718:     def OnCTNClose(self):
laurent@656:         if self._View is not None:
laurent@780:             self._CloseView(self._View)
laurent@774:             self._View = None
etisserant@14:         return True
etisserant@14: 
Edouard@718:     def _doRemoveChild(self, CTNInstance):
Edouard@718:         # Remove all children of child
Edouard@718:         for SubCTNInstance in CTNInstance.IterChildren():
Edouard@718:             CTNInstance._doRemoveChild(SubCTNInstance)
etisserant@14:         # Call the OnCloseMethod
Edouard@718:         CTNInstance.OnCTNClose()
Edouard@717:         # Delete confnode dir
Edouard@2004:         try:
Edouard@2004:             shutil.rmtree(CTNInstance.CTNPath())
andrej@2182:         except Exception:
Edouard@2004:             pass
Edouard@718:         # Remove child of Children
Edouard@718:         self.Children[CTNInstance.CTNType].remove(CTNInstance)
Laurent@1033:         if len(self.Children[CTNInstance.CTNType]) == 0:
Laurent@1033:             self.Children.pop(CTNInstance.CTNType)
etisserant@14:         # Forget it... (View have to refresh)
etisserant@14: 
Edouard@718:     def CTNRemove(self):
Edouard@717:         # Fetch the confnode
andrej@1782:         # CTNInstance = self.GetChildByName(CTNName)
etisserant@14:         # Ask to his parent to remove it
Edouard@718:         self.CTNParent._doRemoveChild(self)
Edouard@718: 
Edouard@718:     def CTNAddChild(self, CTNName, CTNType, IEC_Channel=0):
etisserant@14:         """
Edouard@717:         Create the confnodes that may be added as child to this node self
Edouard@718:         @param CTNType: string desining the confnode class name (get name from CTNChildrenTypes)
Edouard@718:         @param CTNName: string for the name of the confnode instance
Edouard@718:         """
Edouard@720:         # reorganize self.CTNChildrenTypes tuples from (name, CTNClass, Help)
Edouard@718:         # to ( name, (CTNClass, Help)), an make a dict
kinsamanka@3750:         transpose = list(zip(*self.CTNChildrenTypes))
kinsamanka@3750:         CTNChildrenTypes = dict(list(zip(transpose[0], list(zip(transpose[1], transpose[2])))))
Edouard@717:         # Check that adding this confnode is allowed
etisserant@14:         try:
Edouard@718:             CTNClass, CTNHelp = CTNChildrenTypes[CTNType]
etisserant@14:         except KeyError:
andrej@1765:             raise Exception(_("Cannot create child {a1} of type {a2} ").
andrej@1767:                             format(a1=CTNName, a2=CTNType))
andrej@1730: 
Edouard@718:         # if CTNClass is a class factory, call it. (prevent unneeded imports)
andrej@1778:         if isinstance(CTNClass, types.FunctionType):
Edouard@718:             CTNClass = CTNClass()
andrej@1730: 
Edouard@717:         # Eventualy Initialize child instance list for this class of confnode
Edouard@718:         ChildrenWithSameClass = self.Children.setdefault(CTNType, list())
etisserant@14:         # Check count
Edouard@718:         if getattr(CTNClass, "CTNMaxCount", None) and len(ChildrenWithSameClass) >= CTNClass.CTNMaxCount:
kinsamanka@3785: 
kinsamanka@3785:             msg = _("Max count ({a1}) reached for this confnode of type {a2} ").format(
kinsamanka@3785:                     a1=CTNClass.CTNMaxCount, a2=CTNType)
kinsamanka@3785:             self.GetCTRoot().logger.write_warning(msg)
kinsamanka@3785: 
kinsamanka@3785:             return None
andrej@1730: 
Edouard@717:         # create the final class, derived of provided confnode and template
Edouard@718:         class FinalCTNClass(CTNClass, ConfigTreeNode):
etisserant@14:             """
Edouard@718:             ConfNode class is derivated into FinalCTNClass before being instanciated
andrej@1730:             This way __init__ is overloaded to ensure ConfigTreeNode.__init__ is called
Edouard@718:             before CTNClass.__init__, and to do the file related stuff.
etisserant@14:             """
andrej@1868:             def __init__(self, parent):
andrej@1868:                 self.CTNParent = parent
Edouard@717:                 # Keep track of the confnode type name
andrej@1868:                 self.CTNType = CTNType
etisserant@106:                 # remind the help string, for more fancy display
andrej@1868:                 self.CTNHelp = CTNHelp
Edouard@717:                 # Call the base confnode template init - change XSD into class members
andrej@1868:                 ConfigTreeNode.__init__(self)
etisserant@29:                 # check name is unique
andrej@1868:                 NewCTNName = self.FindNewName(CTNName)
etisserant@14:                 # If dir have already be made, and file exist
andrej@1868:                 if os.path.isdir(self.CTNPath(NewCTNName)):  # and os.path.isfile(self.ConfNodeXmlFilePath(CTNName)):
andrej@1733:                     # Load the confnode.xml file into parameters members
andrej@1868:                     self.LoadXMLParams(NewCTNName)
etisserant@20:                     # Basic check. Better to fail immediately.
andrej@1868:                     if self.BaseParams.getName() != NewCTNName:
andrej@1765:                         raise Exception(
andrej@1765:                             _("Project tree layout do not match confnode.xml {a1}!={a2} ").
andrej@1868:                             format(a1=NewCTNName, a2=self.BaseParams.getName()))
Edouard@718: 
Edouard@718:                     # Now, self.CTNPath() should be OK
andrej@1730: 
etisserant@15:                     # Check that IEC_Channel is not already in use.
andrej@1868:                     self.FindNewIEC_Channel(self.BaseParams.getIEC_Channel())
Edouard@717:                     # Call the confnode real __init__
Edouard@718:                     if getattr(CTNClass, "__init__", None):
andrej@1868:                         CTNClass.__init__(self)
andrej@1733:                     # Load and init all the children
andrej@1868:                     self.LoadChildren()
andrej@1733:                     # just loaded, nothing to saved
andrej@1868:                     self.ChangesToSave = False
etisserant@14:                 else:
Edouard@717:                     # If confnode do not have corresponding file/dirs - they will be created on Save
andrej@1868:                     self.CTNMakeDir()
etisserant@14:                     # Find an IEC number
andrej@1868:                     self.FindNewIEC_Channel(IEC_Channel)
Edouard@717:                     # Call the confnode real __init__
Edouard@718:                     if getattr(CTNClass, "__init__", None):
andrej@1868:                         CTNClass.__init__(self)
andrej@1868:                     self.CTNRequestSave()
andrej@1733:                     # just created, must be saved
andrej@1868:                     self.ChangesToSave = True
andrej@1868: 
andrej@1868:             def _getBuildPath(self):
andrej@1868:                 return self.CTNParent._getBuildPath()
andrej@1730: 
etisserant@14:         # Create the object out of the resulting class
andrej@1868:         newConfNodeOpj = FinalCTNClass(self)
Edouard@718:         # Store it in CTNgedChils
Edouard@718:         ChildrenWithSameClass.append(newConfNodeOpj)
andrej@1730: 
Edouard@717:         return newConfNodeOpj
andrej@1730: 
Edouard@718:     def ClearChildren(self):
Edouard@718:         for child in self.IterChildren():
Edouard@718:             child.ClearChildren()
Edouard@718:         self.Children = {}
andrej@1730: 
andrej@1744:     def LoadXMLParams(self, CTNName=None):
Edouard@718:         methode_name = os.path.join(self.CTNPath(CTNName), "methods.py")
etisserant@105:         if os.path.isfile(methode_name):
kinsamanka@3750:             exec(compile(open(methode_name, "rb").read(), methode_name, 'exec'))
andrej@1730: 
Laurent@1332:         ConfNodeName = CTNName if CTNName is not None else self.CTNName()
andrej@1730: 
lbessard@17:         # Get the base xml tree
etisserant@20:         if self.MandatoryParams:
etisserant@203:             try:
Edouard@718:                 basexmlfile = open(self.ConfNodeBaseXmlFilePath(CTNName), 'r')
Laurent@1330:                 self.BaseParams, error = _BaseParamsParser.LoadXMLString(basexmlfile.read())
Laurent@1330:                 if error is not None:
andrej@1581:                     (fname, lnum, src) = ((ConfNodeName + " BaseParams",) + error)
andrej@1744:                     self.GetCTRoot().logger.write_warning(XSDSchemaErrorMessage.format(a1=fname, a2=lnum, a3=src))
Laurent@1315:                 self.MandatoryParams = ("BaseParams", self.BaseParams)
etisserant@106:                 basexmlfile.close()
andrej@2418:             except Exception as exc:
kinsamanka@3752:                 msg = _("Couldn't load confnode base parameters {a1} :\n {a2}").format(a1=ConfNodeName, a2=str(exc))
andrej@1581:                 self.GetCTRoot().logger.write_error(msg)
Edouard@718:                 self.GetCTRoot().logger.write_error(traceback.format_exc())
andrej@1730: 
etisserant@14:         # Get the xml tree
Edouard@718:         if self.CTNParams:
etisserant@203:             try:
Edouard@718:                 xmlfile = open(self.ConfNodeXmlFilePath(CTNName), 'r')
Laurent@1330:                 obj, error = self.Parser.LoadXMLString(xmlfile.read())
Laurent@1330:                 if error is not None:
andrej@1581:                     (fname, lnum, src) = ((ConfNodeName,) + error)
andrej@1744:                     self.GetCTRoot().logger.write_warning(XSDSchemaErrorMessage.format(a1=fname, a2=lnum, a3=src))
Laurent@1315:                 name = obj.getLocalTag()
Laurent@1315:                 setattr(self, name, obj)
Laurent@1315:                 self.CTNParams = (name, obj)
etisserant@106:                 xmlfile.close()
andrej@2418:             except Exception as exc:
kinsamanka@3752:                 msg = _("Couldn't load confnode parameters {a1} :\n {a2}").format(a1=ConfNodeName, a2=str(exc))
andrej@1581:                 self.GetCTRoot().logger.write_error(msg)
Edouard@718:                 self.GetCTRoot().logger.write_error(traceback.format_exc())
andrej@1730: 
Edouard@718:     def LoadChildren(self):
Edouard@718:         # Iterate over all CTNName@CTNType in confnode directory, and try to open them
Edouard@718:         for CTNDir in os.listdir(self.CTNPath()):
Edouard@718:             if os.path.isdir(os.path.join(self.CTNPath(), CTNDir)) and \
Edouard@718:                CTNDir.count(NameTypeSeparator) == 1:
Edouard@718:                 pname, ptype = CTNDir.split(NameTypeSeparator)
etisserant@203:                 try:
Edouard@718:                     self.CTNAddChild(pname, ptype)
andrej@2418:                 except Exception as exc:
kinsamanka@3752:                     msg = _("Could not add child \"{a1}\", type {a2} :\n{a3}\n").format(a1=pname, a2=ptype, a3=str(exc))
andrej@1581:                     self.GetCTRoot().logger.write_error(msg)
Edouard@718:                     self.GetCTRoot().logger.write_error(traceback.format_exc())
Edouard@2640: 
Edouard@2640: 
Edouard@2640:     def FatalError(self, message):
Edouard@2640:         """ Raise an exception that will trigger error message intended to 
Edouard@2640:             the user, but without backtrace since it is not a software error """
Edouard@2640: 
Edouard@2640:         raise UserAddressedException(message)
Edouard@2640: