Changing Cut/Copy/Paste procedures for using wx.Clipboard with xml definition of copied elements
authorlaurent
Fri, 24 Jul 2009 11:07:33 +0200
changeset 384 ed27a676d5c9
parent 383 25ffba02b6a8
child 385 373635372b93
Changing Cut/Copy/Paste procedures for using wx.Clipboard with xml definition of copied elements
PLCControler.py
PLCOpenEditor.py
Viewer.py
graphics/GraphicCommons.py
plcopen/__init__.py
plcopen/plcopen.py
--- a/PLCControler.py	Fri Jul 24 10:47:35 2009 +0200
+++ b/PLCControler.py	Fri Jul 24 11:07:33 2009 +0200
@@ -1580,6 +1580,148 @@
             return self.GetProjectPouVariables(words[1], debug)
         return []
 
+    def GetEditedElementCopy(self, tagname, debug = False):
+        element = self.GetEditedElement(tagname, debug)
+        if element is not None:
+            name = element.__class__.__name__
+            return element.generateXMLText(name.split("_")[-1], 0)
+        return ""
+        
+    def GetEditedElementInstancesCopy(self, tagname, blocks_id = None, wires = None, debug = False):
+        element = self.GetEditedElement(tagname, debug)
+        text = ""
+        if element is not None:
+            wires = dict([(wire, True) for wire in wires if wire[0] in blocks_id and wire[1] in blocks_id])
+            for id in blocks_id:
+                instance = element.getinstance(id)
+                if instance is not None:
+                    instance_copy = self.Copy(instance)
+                    instance_copy.filterConnections(wires)
+                    name = instance_copy.__class__.__name__
+                    text += instance_copy.generateXMLText(name.split("_")[-1], 0)
+        return text
+    
+    def GenerateNewName(self, tagname, name, format, exclude={}, debug=False):
+        names = exclude.copy()
+        names.update(dict([(varname.upper(), True) for varname in self.GetEditedElementVariables(tagname, debug)]))
+        element = self.GetEditedElement(tagname, debug)
+        if element is not None:
+            for instance in element.getinstances():
+                if isinstance(instance, (plcopen.sfcObjects_step, plcopen.commonObjects_connector, plcopen.commonObjects_continuation)):
+                    names[instance.getname()] = True
+        i = 1
+        while names.get(name.upper(), False):
+            name = (format%i)
+            i += 1
+        return name
+    
+    CheckPasteCompatibility = {"SFC": lambda name: True,
+                               "LD": lambda name: not name.startswith("sfcObjects"),
+                               "FBD": lambda name: name.startswith("fbdObjects") or name.startswith("commonObjects")}
+    
+    def PasteEditedElementInstances(self, tagname, text, new_pos, middle=False, debug=False):
+        element = self.GetEditedElement(tagname, debug)
+        element_name, element_type = self.GetEditedElementType(tagname, debug)
+        if element is not None:
+            bodytype = element.getbodyType()
+            
+            # Get edited element type scaling
+            scaling = None
+            project = self.GetProject(debug)
+            if project is not None:
+                properties = project.getcontentHeader()
+                scaling = properties["scaling"][bodytype]
+            
+            # Get ids already by all the instances in edited element
+            used_id = dict([(instance.getlocalId(), True) for instance in element.getinstances()])
+            new_id = {}
+            
+            text = "<paste>%s</paste>"%text
+            
+            try:
+                tree = minidom.parseString(text)
+            except:
+                return _("Invalid plcopen element(s)!!!")
+            instances = []
+            exclude = {}
+            for root in tree.childNodes:
+                if root.nodeType == tree.ELEMENT_NODE and root.nodeName == "paste":
+                    for child in root.childNodes:
+                        if child.nodeType == tree.ELEMENT_NODE:
+                            classname = plcopen.ElementNameToClass[child.nodeName]
+                            if not self.CheckPasteCompatibility[bodytype](classname):
+                                return _("\"%s\" element can't be paste here!!!")%child.nodeName
+                            classobj = getattr(plcopen, classname, None)
+                            if classobj is not None:
+                                instance = classobj()
+                                instance.loadXMLTree(child)
+                                if child.nodeName == "block":
+                                    blockname = instance.getinstanceName()
+                                    if blockname is not None:
+                                        blocktype = instance.gettypeName()
+                                        if element_type == "function":
+                                            return _("FunctionBlock \"%s\" can't be paste in a Function!!!")%blocktype
+                                        blockname = self.GenerateNewName(tagname, blockname, "Block%d", debug=debug)
+                                        exclude[blockname] = True
+                                        instance.setinstanceName(blockname)
+                                        self.AddEditedElementPouVar(tagname, blocktype, blockname)
+                                elif child.nodeName == "step":
+                                    stepname = self.GenerateNewName(tagname, instance.getname(), "Step%d", exclude, debug)
+                                    exclude[stepname] = True
+                                    instance.setname(stepname)
+                                localid = instance.getlocalId()
+                                if not used_id.has_key(localid):
+                                    new_id[localid] = True
+                                instances.append((child.nodeName, instance))
+            
+            if len(instances) == 0:
+                return _("Invalid plcopen element(s)!!!")
+            
+            idx = 1
+            translate_id = {}
+            bbox = plcopen.rect()
+            for name, instance in instances:
+                localId = instance.getlocalId()
+                bbox.union(instance.getBoundingBox())
+                if used_id.has_key(localId):
+                    while used_id.has_key(idx) or new_id.has_key(idx):
+                        idx += 1
+                    new_id[idx] = True
+                    instance.setlocalId(idx)
+                    translate_id[localId] = idx
+            
+            x, y, width, height = bbox.bounding_box()
+            if middle:
+                new_pos[0] -= width / 2
+                new_pos[1] -= height / 2
+            else:
+                new_pos = map(lambda x: x + 30, new_pos)
+            if scaling[0] != 0 and scaling[1] != 0:
+                min_pos = map(lambda x: 30 / x, scaling)
+                minx = round(min_pos[0])
+                if int(min_pos[0]) == round(min_pos[0]):
+                    minx += 1
+                miny = round(min_pos[1])
+                if int(min_pos[1]) == round(min_pos[1]):
+                    miny += 1
+                minx *= scaling[0]
+                miny *= scaling[1]
+                new_pos = (max(minx, round(new_pos[0] / scaling[0]) * scaling[0]),
+                           max(miny, round(new_pos[1] / scaling[1]) * scaling[1]))
+            else:
+                new_pos = (max(30, new_pos[0]), max(30, new_pos[1]))
+            diff = (new_pos[0] - x, new_pos[1] - y)
+            
+            connections = {}
+            for name, instance in instances:
+                connections.update(instance.updateConnectionsId(translate_id))
+                if getattr(instance, "setexecutionOrderId", None) is not None:
+                    instance.setexecutionOrderId(0)
+                instance.translate(*diff)
+                element.addinstance(name, instance)
+            
+            return new_id, connections
+                
     # Return the current pou editing informations
     def GetEditedElementInstanceInfos(self, tagname, id = None, exclude = [], debug = False):
         infos = {}
--- a/PLCOpenEditor.py	Fri Jul 24 10:47:35 2009 +0200
+++ b/PLCOpenEditor.py	Fri Jul 24 11:07:33 2009 +0200
@@ -177,8 +177,7 @@
 
 class PLCOpenEditor(wx.Frame):
     
-    CopyBuffer = None
-    
+    # Compatibility function for wx versions < 2.6
     if wx.VERSION < (2, 6, 0):
         def Bind(self, event, function, id = None):
             if id is not None:
@@ -689,10 +688,21 @@
         event.Skip()
 
     def GetCopyBuffer(self):
-        return PLCOpenEditor.CopyBuffer
-    
-    def SetCopyBuffer(self, element):
-        PLCOpenEditor.CopyBuffer = element
+        data = None
+        if wx.TheClipboard.Open():
+            dataobj = wx.TextDataObject()
+            if wx.TheClipboard.GetData(dataobj):
+                data = dataobj.GetText()
+            wx.TheClipboard.Close()
+        return data
+        
+    def SetCopyBuffer(self, text):
+        if wx.TheClipboard.Open():
+            data = wx.TextDataObject()
+            data.SetText(text)
+            wx.TheClipboard.SetData(data)
+            wx.TheClipboard.Flush()
+            wx.TheClipboard.Close()
         self.RefreshEditMenu()
 
     def GetDrawingMode(self):
@@ -1071,7 +1081,7 @@
                 if self.TabsOpened.GetPageCount() > 0:
                     self.EditMenu.Enable(wx.ID_CUT, True)
                     self.EditMenu.Enable(wx.ID_COPY, True)
-                    if self.CopyBuffer is not None:
+                    if self.GetCopyBuffer() is not None:
                         self.EditMenu.Enable(wx.ID_PASTE, True)
                     else:
                         self.EditMenu.Enable(wx.ID_PASTE, False)
--- a/Viewer.py	Fri Jul 24 10:47:35 2009 +0200
+++ b/Viewer.py	Fri Jul 24 11:07:33 2009 +0200
@@ -2564,7 +2564,9 @@
     
     def Cut(self):
         if not self.Debug and (self.IsBlock(self.SelectedElement) or self.IsComment(self.SelectedElement) or isinstance(self.SelectedElement, Graphic_Group)):
-            self.ParentWindow.SetCopyBuffer(self.SelectedElement.Clone(self))
+            blocks, wires = self.SelectedElement.GetDefinition()
+            text = self.Controler.GetEditedElementInstancesCopy(self.TagName, blocks, wires, self.Debug)
+            self.ParentWindow.SetCopyBuffer(text)
             rect = self.SelectedElement.GetRedrawRect(1, 1)
             self.SelectedElement.Delete()
             self.SelectedElement = None
@@ -2576,42 +2578,30 @@
         
     def Copy(self):
         if not self.Debug and (self.IsBlock(self.SelectedElement) or self.IsComment(self.SelectedElement) or isinstance(self.SelectedElement, Graphic_Group)):
-            self.ParentWindow.SetCopyBuffer(self.SelectedElement.Clone(self))
+            blocks, wires = self.SelectedElement.GetDefinition()
+            text = self.Controler.GetEditedElementInstancesCopy(self.TagName, blocks, wires, self.Debug)
+            self.ParentWindow.SetCopyBuffer(text)
             
     def Paste(self):
-        element = self.ParentWindow.GetCopyBuffer()
-        if not self.Debug and element is not None and self.CanAddElement(element):
+        if not self.Debug:
+            element = self.ParentWindow.GetCopyBuffer()
             mouse_pos = self.ScreenToClient(wx.GetMousePosition())
-            if wx.Rect(0, 0, *self.GetClientSize()).InsideXY(mouse_pos.x, mouse_pos.y):
+            middle = wx.Rect(0, 0, *self.GetClientSize()).InsideXY(mouse_pos.x, mouse_pos.y)
+            if middle:
                 x, y = self.CalcUnscrolledPosition(mouse_pos.x, mouse_pos.y)
-                block_size = element.GetSize()
-                x = int(float(x) / self.ViewScale[0]) - block_size[0] / 2
-                y = int(float(y) / self.ViewScale[1]) - block_size[1] / 2
             else:
                 x, y = self.CalcUnscrolledPosition(0, 0)
-                x = int(x / self.ViewScale[0]) + 30
-                y = int(y / self.ViewScale[1]) + 30
-            if self.Scaling is not None:
-                new_pos = wx.Point(max(round_scaling(30, self.Scaling[0], 1), 
-                                       round_scaling(x, self.Scaling[0])),
-                                   max(round_scaling(30, self.Scaling[1], 1),
-                                       round_scaling(y, self.Scaling[1])))
+            new_pos = [int(x / self.ViewScale[0]), int(y / self.ViewScale[1])]
+            result = self.Controler.PasteEditedElementInstances(self.TagName, element, new_pos, middle, self.Debug)
+            if not isinstance(result, (StringType, UnicodeType)):
+                self.RefreshBuffer()
+                self.RefreshView(result)
+                self.ParentWindow.RefreshVariablePanel(self.TagName)
+                self.ParentWindow.RefreshInstancesTree()
             else:
-                new_pos = wx.Point(max(30, x), max(30, y))
-            block = self.CopyBlock(element, new_pos)
-            self.RefreshVisibleElements()
-            if self.SelectedElement is not None:
-                self.SelectedElement.SetSelected(False)
-            self.SelectedElement = block
-            self.SelectedElement.SetSelected(True)
-            self.RefreshBuffer()
-            self.RefreshScrollBars()
-            self.ParentWindow.RefreshVariablePanel(self.TagName)
-            self.ParentWindow.RefreshInstancesTree()
-        else:
-            message = wx.MessageDialog(self, "You can't paste the element in buffer here!", "Error", wx.OK|wx.ICON_ERROR)
-            message.ShowModal()
-            message.Destroy()
+                message = wx.MessageDialog(self, result, "Error", wx.OK|wx.ICON_ERROR)
+                message.ShowModal()
+                message.Destroy()
 
     def CanAddElement(self, block):
         if isinstance(block, Graphic_Group):
@@ -2625,17 +2615,11 @@
         return False
 
     def GenerateNewName(self, element, exclude={}):
-        names = exclude.copy()
-        if isinstance(element, FBD_Block):
-            names.update(dict([(varname.upper(), True) for varname in self.Controler.GetEditedElementVariables(self.TagName, self.Debug)]))
-            format = "Block%d"
-        elif isinstance(element, SFC_Step):
-            names.update(dict([(block.GetName().upper(), True) for block in self.Blocks if isinstance(block, SFC_Step)]))
+        if isinstance(element, SFC_Step):
             format = "Step%d"
-        i = 1
-        while names.get((format%i).upper(), False):
-            i += 1
-        return format%i
+        else:
+            format = "Block%d"    
+        return self.Controler.GenerateNewName(self.TagName, element.GetName(), format, exclude, self.Debug)
 
     def IsNamedElement(self, element):
         return isinstance(element, FBD_Block) and element.GetName() != "" or isinstance(element, SFC_Step)
--- a/graphics/GraphicCommons.py	Fri Jul 24 10:47:35 2009 +0200
+++ b/graphics/GraphicCommons.py	Fri Jul 24 11:07:33 2009 +0200
@@ -459,6 +459,9 @@
         self.BoundingBox = wx.Rect(0, 0, 0, 0)
         self.Visible = False
     
+    def GetDefinition(self):
+        return [self.Id], []
+    
     def TestVisible(self, screen):
         self.Visible = self.GetRedrawRect().Intersects(screen)
     
@@ -850,6 +853,15 @@
     def __del__(self):
         self.Elements = []
     
+    def GetDefinition(self):
+        blocks = [] 
+        wires = []
+        for element in self.Elements:
+            block, wire = element.GetDefinition()
+            blocks.extend(block)
+            wires.extend(wire)
+        return blocks, wires
+    
     # Make a clone of this element
     def Clone(self, parent, pos = None):
         group = Graphic_Group(parent)
@@ -1538,7 +1550,15 @@
         self.OverEnd = False
         self.ComputingType = False
         self.ToolTip = None
-        
+        self.Font = parent.GetMiniFont()
+    
+    def GetDefinition(self):
+        if self.StartConnected is not None and self.EndConnected is not None:
+            startblock = self.StartConnected.GetParentBlock()
+            endblock = self.EndConnected.GetParentBlock()
+            return [], [(startblock.GetId(), endblock.GetId())]
+        return [], []
+    
     def Flush(self):
         self.StartConnected = None
         self.EndConnected = None
--- a/plcopen/__init__.py	Fri Jul 24 10:47:35 2009 +0200
+++ b/plcopen/__init__.py	Fri Jul 24 11:07:33 2009 +0200
@@ -28,6 +28,6 @@
 import plcopen
 for classname, cls in plcopen.PLCOpenClasses.items():
     plcopen.__dict__[classname] = cls
-from plcopen import VarOrder
+from plcopen import VarOrder, ElementNameToClass, rect
 
 from structures import *
--- a/plcopen/plcopen.py	Fri Jul 24 10:47:35 2009 +0200
+++ b/plcopen/plcopen.py	Fri Jul 24 11:07:33 2009 +0200
@@ -45,8 +45,54 @@
 QualifierList = {"N" : False, "R" : False, "S" : False, "L" : True, "D" : True, 
     "P" : False, "P0" : False, "P1" : False, "SD" : True, "DS" : True, "SL" : True}
 
+
+def _init_and_compare(function, v1, v2):
+    if v1 is None:
+        return v2
+    if v2 is not None:
+        return function(v1, v2)
+    return v1
+
+"""
+Helper class for bounding_box calculation 
+"""
+class rect:
+    
+    def __init__(self, x=None, y=None, width=None, height=None):
+        self.x_min = x
+        self.x_max = None
+        self.y_min = y
+        self.y_max = None
+        if width is not None and x is not None:
+            self.x_max = x + width
+        if height is not None and y is not None:
+            self.y_max = y + height
+    
+    def update(self, x, y):
+        self.x_min = _init_and_compare(min, self.x_min, x)
+        self.x_max = _init_and_compare(max, self.x_max, x)
+        self.y_min = _init_and_compare(min, self.y_min, y)
+        self.y_max = _init_and_compare(max, self.y_max, y)
+        
+    def union(self, rect):
+        self.x_min = _init_and_compare(min, self.x_min, rect.x_min)
+        self.x_max = _init_and_compare(max, self.x_max, rect.x_max)
+        self.y_min = _init_and_compare(min, self.y_min, rect.y_min)
+        self.y_max = _init_and_compare(max, self.y_max, rect.y_max)
+    
+    def bounding_box(self):
+        width = height = None
+        if self.x_min is not None and self.x_max is not None:
+            width = self.x_max - self.x_min
+        if self.y_min is not None and self.y_max is not None:
+            height = self.y_max - self.y_min
+        return self.x_min, self.y_min, width, height
+            
+
 PLCOpenClasses = GenerateClassesFromXSD(os.path.join(os.path.split(__file__)[0], "TC6_XML_V10_B.xsd"))
 
+ElementNameToClass = {}
+
 cls = PLCOpenClasses.get("formattedText", None)
 if cls:
     def updateElementName(self, old_name, new_name):
@@ -1223,10 +1269,120 @@
 def sety(self, y):
     self.position.sety(y)
 
+def _getBoundingBox(self):
+    return rect(self.getx(), self.gety(), self.getwidth(), self.getheight())
+
+def _getConnectionsBoundingBox(connectionPointIn):
+    bbox = rect()
+    connections = connectionPointIn.getconnections()
+    if connections is not None:
+        for connection in connections:
+            for x, y in connection.getpoints():
+                bbox.update(x, y)
+    return bbox
+
+def _getBoundingBoxSingle(self):
+    bbox = _getBoundingBox(self)
+    if self.connectionPointIn is not None:
+        bbox.union(_getConnectionsBoundingBox(self.connectionPointIn))
+    return bbox
+
+def _getBoundingBoxMultiple(self):
+    bbox = _getBoundingBox(self)
+    for connectionPointIn in self.getconnectionPointIn():
+        bbox.union(_getConnectionsBoundingBox(connectionPointIn))
+    return bbox
+
+def _filterConnections(connectionPointIn, localId, connections):
+    in_connections = connectionPointIn.getconnections()
+    if in_connections is not None:
+        to_delete = []
+        for i, connection in enumerate(in_connections):
+            connected = connection.getrefLocalId()
+            if not connections.has_key((localId, connected)) and \
+               not connections.has_key((connected, localId)):
+                to_delete.append(i)
+        to_delete.reverse()
+        for i in to_delete:
+            connectionPointIn.removeconnection(i)
+
+def _filterConnectionsSingle(self, connections):
+    if self.connectionPointIn is not None:
+        _filterConnections(self.connectionPointIn, self.localId, connections)
+
+def _filterConnectionsMultiple(self, connections):
+    for connectionPointIn in self.getconnectionPointIn():
+        _filterConnections(connectionPointIn, self.localId, connections)
+
+def _getconnectionsdefinition(instance, connections_end):
+    id = instance.getlocalId()
+    return dict([((id, end), True) for end in connections_end])
+
+def _updateConnectionsId(connectionPointIn, translation):
+    connections_end = []
+    connections = connectionPointIn.getconnections()
+    if connections is not None:
+        for connection in connections:
+            refLocalId = connection.getrefLocalId()
+            new_reflocalId = translation.get(refLocalId, refLocalId)
+            connection.setrefLocalId(new_reflocalId)
+            connections_end.append(new_reflocalId)
+    return connections_end
+
+def _updateConnectionsIdSingle(self, translation):
+    connections_end = []
+    if self.connectionPointIn is not None:
+        connections_end = _updateConnectionsId(self.connectionPointIn, translation)
+    return _getconnectionsdefinition(self, connections_end)
+
+def _updateConnectionsIdMultiple(self, translation):
+    connections_end = []
+    for connectionPointIn in self.getconnectionPointIn():
+        connections_end.extend(_updateConnectionsId(connectionPointIn, translation))
+    return _getconnectionsdefinition(self, connections_end)
+
+def _translate(self, dx, dy):
+    self.setx(self.getx() + dx)
+    self.sety(self.gety() + dy)
+    
+def _translateConnections(connectionPointIn, dx, dy):
+    connections = connectionPointIn.getconnections()
+    if connections is not None:
+        for connection in connections:
+            for position in connection.getposition():
+                position.setx(position.getx() + dx)
+                position.sety(position.gety() + dy)
+
+def _translateSingle(self, dx, dy):
+    _translate(self, dx, dy)
+    if self.connectionPointIn is not None:
+        _translateConnections(self.connectionPointIn, dx, dy)
+
+def _translateMultiple(self, dx, dy):
+    _translate(self, dx, dy)
+    for connectionPointIn in self.getconnectionPointIn():
+        _translateConnections(connectionPointIn, dx, dy)
+
 def _updateElementName(self, old_name, new_name):
     pass
 
+_connectionsFunctions = {
+    "bbox": {"none": _getBoundingBox,
+             "single": _getBoundingBoxSingle,
+             "multiple": _getBoundingBoxMultiple},
+    "translate": {"none": _translate,
+               "single": _translateSingle,
+               "multiple": _translateMultiple},
+    "filter": {"none": lambda self, connections: None,
+               "single": _filterConnectionsSingle,
+               "multiple": _filterConnectionsMultiple},
+    "update": {"none": lambda self, translation: None,
+               "single": _updateConnectionsIdSingle,
+               "multiple": _updateConnectionsIdMultiple}
+}
+
 def _initElementClass(name, classname, connectionPointInType="none"):
+    ElementNameToClass[name] = classname
     cls = PLCOpenClasses.get(classname, None)
     if cls:
         setattr(cls, "getx", getx)
@@ -1234,6 +1390,10 @@
         setattr(cls, "setx", setx)
         setattr(cls, "sety", sety)
         setattr(cls, "updateElementName", _updateElementName)
+        setattr(cls, "getBoundingBox", _connectionsFunctions["bbox"][connectionPointInType])
+        setattr(cls, "translate", _connectionsFunctions["translate"][connectionPointInType])
+        setattr(cls, "filterConnections", _connectionsFunctions["filter"][connectionPointInType])
+        setattr(cls, "updateConnectionsId", _connectionsFunctions["update"][connectionPointInType])
     return cls
 
 def _getexecutionOrder(instance, specific_values):
@@ -1414,6 +1574,24 @@
             self.typeName = new_name
     setattr(cls, "updateElementName", updateElementName)
 
+    def filterConnections(self, connections):
+        for input in self.inputVariables.getvariable():
+            _filterConnections(input.connectionPointIn, self.localId, connections)
+    setattr(cls, "filterConnections", filterConnections)
+
+    def updateConnectionsId(self, translation):
+        connections_end = []
+        for input in self.inputVariables.getvariable():
+            connections_end.extend(_updateConnectionsId(input.connectionPointIn, translation))
+        return _getconnectionsdefinition(self, connections_end)
+    setattr(cls, "updateConnectionsId", updateConnectionsId)
+
+    def translate(self, dx, dy):
+        _translate(self, dx, dy)
+        for input in self.inputVariables.getvariable():
+            _translateConnections(input.connectionPointIn, dx, dy)
+    setattr(cls, "translate", translate)
+
 cls = _initElementClass("leftPowerRail", "ldObjects_leftPowerRail")
 if cls:
     setattr(cls, "getinfos", _getpowerrailinfosFunction("leftPowerRail"))