Implemented functions to save the UML graphics to disc.

Sun, 09 Sep 2012 14:46:59 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 09 Sep 2012 14:46:59 +0200
changeset 2030
db11a2fe9bbc
parent 2028
30247d523fdb
child 2031
c36c2eb62a75

Implemented functions to save the UML graphics to disc.

Graphics/ApplicationDiagram.py file | annotate | diff | comparison | revisions
Graphics/AssociationItem.py file | annotate | diff | comparison | revisions
Graphics/ClassItem.py file | annotate | diff | comparison | revisions
Graphics/ImportsDiagram.py file | annotate | diff | comparison | revisions
Graphics/ModuleItem.py file | annotate | diff | comparison | revisions
Graphics/PackageDiagram.py file | annotate | diff | comparison | revisions
Graphics/PackageItem.py file | annotate | diff | comparison | revisions
Graphics/UMLClassDiagram.py file | annotate | diff | comparison | revisions
Graphics/UMLDialog.py file | annotate | diff | comparison | revisions
Graphics/UMLGraphicsView.py file | annotate | diff | comparison | revisions
Graphics/UMLItem.py file | annotate | diff | comparison | revisions
Utilities/__init__.py file | annotate | diff | comparison | revisions
icons/default/fileSavePixmap.png file | annotate | diff | comparison | revisions
--- a/Graphics/ApplicationDiagram.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/ApplicationDiagram.py	Sun Sep 09 14:46:59 2012 +0200
@@ -39,7 +39,8 @@
         self.project = project
         self.noModules = noModules
         
-        UMLDialog.__init__(self, buildFunction=self.__buildPackages, parent=parent)
+        UMLDialog.__init__(self, "ApplicationDiagram", buildFunction=self.__buildPackages,
+            parent=parent)
         self.setDiagramName(
             self.trUtf8("Application Diagram {0}").format(project.getProjectName()))
         
@@ -48,6 +49,9 @@
         else:
             self.setObjectName(name)
         
+        self.umlView.setPersistenceData(
+            "project={0}".format(self.project.getProjectFile()))
+        
         self.umlView.relayout.connect(self.relayout)
         
     def __buildModulesDict(self):
@@ -228,6 +232,7 @@
         modules.sort()
         pm = PackageModel(name, modules)
         pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene)
+        pw.setId(self.umlView.getItemId())
         return pw
         
     def __createAssociations(self, shapes):
--- a/Graphics/AssociationItem.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/AssociationItem.py	Sun Sep 09 14:46:59 2012 +0200
@@ -12,10 +12,14 @@
 
 from E5Graphics.E5ArrowItem import E5ArrowItem, NormalArrow, WideArrow
 
+import Utilities
+
+
 Normal = 0
 Generalisation = 1
 Imports = 2
 
+
 NoRegion = 0
 West = 1
 North = 2
@@ -77,6 +81,7 @@
         self.itemA = itemA
         self.itemB = itemB
         self.assocType = type
+        self.topToBottom = topToBottom
         
         self.regionA = NoRegion
         self.regionB = NoRegion
@@ -504,3 +509,48 @@
         """
         self.itemA.removeAssociation(self)
         self.itemB.removeAssociation(self)
+    
+    def buildAssociationItemDataString(self):
+        """
+        Public method to build a string to persist the specific item data.
+        
+        This string should be built like "attribute=value" with pairs separated
+        by ", ". value must not contain ", " or newlines.
+        
+        @return persistence data (string)
+        """
+        entries = [
+            "src={0}".format(self.itemA.getId()),
+            "dst={0}".format(self.itemB.getId()),
+            "type={0}".format(self.assocType),
+            "topToBottom={0}".format(self.topToBottom)
+        ]
+        return ", ".join(entries)
+    
+    @classmethod
+    def parseAssociationItemDataString(cls, data):
+        """
+        Class method to parse the given persistence data.
+        
+        @param data persisted data to be parsed (string)
+        @return tuple with the IDs of the source and destination items,
+            the association type and a flag indicating to associate from top
+            to bottom (integer, integer, integer, boolean)
+        """
+        src = -1
+        dst = -1
+        assocType = Normal
+        topToBottom = False
+        for entry in data.split(", "):
+            if "=" in entry:
+                key, value = entry.split("=", 1)
+                if key == "src":
+                    src = int(value)
+                elif key == "dst":
+                    dst = int(value)
+                elif key == "type":
+                    assocType = value
+                elif key == "topToBottom":
+                    topToBottom = Utilities.toBool(value)
+        
+        return src, dst, assocType, topToBottom
--- a/Graphics/ClassItem.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/ClassItem.py	Sun Sep 09 14:46:59 2012 +0200
@@ -215,3 +215,36 @@
         @return external state (boolean)
         """
         return self.external
+    
+    def buildItemDataString(self):
+        """
+        Public method to build a string to persist the specific item data.
+        
+        This string must start with ", " and should be built like
+        "attribute=value" with pairs separated by ", ". value must not contain ", "
+        or newlines.
+        
+        @return persistence data (string)
+        """
+        entries = [
+            "item_type=class",
+            "is_external={0}".format(self.external),
+            "no_attributes={0}".format(self.noAttrs),
+            "name={0}".format(self.model.getName()),
+        ]
+        attributes = self.model.getAttributes()
+        if attributes:
+            entries.append("attributes={0}".format("||".join(attributes)))
+        methods = self.model.getMethods()
+        if methods:
+            entries.append("methods={0}".format("||".join(methods)))
+        
+        return ", " + ", ".join(entries)
+    
+    def parseItemDataString(self, data):
+        """
+        Public method to parse the given persistence data.
+        
+        @param data persisted data to be parsed (string)
+        """
+        # TODO: implement this
--- a/Graphics/ImportsDiagram.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/ImportsDiagram.py	Sun Sep 09 14:46:59 2012 +0200
@@ -41,7 +41,8 @@
         @keyparam showExternalImports flag indicating to show exports from outside
             the package (boolean)
         """
-        UMLDialog.__init__(self, buildFunction=self.__buildImports, parent=parent)
+        UMLDialog.__init__(self, "ImportsDiagram", buildFunction=self.__buildImports,
+            parent=parent)
         
         self.showExternalImports = showExternalImports
         self.packagePath = Utilities.normabspath(package)
@@ -53,6 +54,8 @@
             hasInit = len(glob.glob(os.path.join(ppath, '__init__.*'))) > 0
         self.shortPackage = self.packagePath.replace(ppath, '').replace(os.sep, '.')[1:]
         
+        self.umlView.setPersistenceData("package={0}".format(self.packagePath))
+        
         pname = project.getProjectName()
         if pname:
             name = self.trUtf8("Imports Diagramm {0}: {1}").format(
@@ -226,6 +229,7 @@
         classes.sort()
         impM = ModuleModel(name, classes)
         impW = ModuleItem(impM, x, y, scene=self.scene)
+        impW.setId(self.umlView.getItemId())
         return impW
         
     def __createAssociations(self, shapes):
--- a/Graphics/ModuleItem.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/ModuleItem.py	Sun Sep 09 14:46:59 2012 +0200
@@ -161,3 +161,31 @@
         painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y)
         
         self.adjustAssociations()
+    
+    def buildItemDataString(self):
+        """
+        Public method to build a string to persist the specific item data.
+        
+        This string must start with ", " and should be built like
+        "attribute=value" with pairs separated by ", ". value must not contain ", "
+        or newlines.
+        
+        @return persistence data (string)
+        """
+        entries = [
+            "item_type=module",
+            "name={0}".format(self.model.getName()),
+        ]
+        classes = self.model.getClasses()
+        if classes:
+            entries.append("classes={0}".format("||".join(classes)))
+        
+        return ", " + ", ".join(entries)
+    
+    def parseItemDataString(self, data):
+        """
+        Public method to parse the given persistence data.
+        
+        @param data persisted data to be parsed (string)
+        """
+        # TODO: implement this
--- a/Graphics/PackageDiagram.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/PackageDiagram.py	Sun Sep 09 14:46:59 2012 +0200
@@ -37,12 +37,15 @@
         @param name name of the view widget (string)
         @keyparam noAttrs flag indicating, that no attributes should be shown (boolean)
         """
-        UMLDialog.__init__(self, buildFunction=self.__buildClasses, parent=parent)
+        UMLDialog.__init__(self, "PackageDiagram", buildFunction=self.__buildClasses,
+            parent=parent)
         
         self.package = Utilities.normabspath(package)
         self.allClasses = {}
         self.noAttrs = noAttrs
         
+        self.umlView.setPersistenceData("package={0}".format(self.package))
+        
         pname = project.getProjectName()
         if pname:
             name = self.trUtf8("Package Diagram {0}: {1}").format(
@@ -281,6 +284,7 @@
             name = "{0} (Module)".format(name)
         cl = ClassModel(name, meths[:], attrs[:])
         cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene)
+        cw.setId(self.umlView.getItemId())
         self.allClasses[className] = cw
         
     def __addExternalClass(self, _class, x, y):
@@ -296,6 +300,7 @@
         """
         cl = ClassModel(_class)
         cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene)
+        cw.setId(self.umlView.getItemId())
         self.allClasses[_class] = cw
         
     def __createAssociations(self, routes):
--- a/Graphics/PackageItem.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/PackageItem.py	Sun Sep 09 14:46:59 2012 +0200
@@ -180,3 +180,32 @@
         painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y)
         
         self.adjustAssociations()
+    
+    def buildItemDataString(self):
+        """
+        Public method to build a string to persist the specific item data.
+        
+        This string must start with ", " and should be built like
+        "attribute=value" with pairs separated by ", ". value must not contain ", "
+        or newlines.
+        
+        @return persistence data (string)
+        """
+        entries = [
+            "item_type=package",
+            "no_modules={0}".format(self.noModules),
+            "name={0}".format(self.model.getName()),
+        ]
+        modules = self.model.getModules()
+        if modules:
+            entries.append("modules={0}".format("||".join(modules)))
+        
+        return ", " + ", ".join(entries)
+    
+    def parseItemDataString(self, data):
+        """
+        Public method to parse the given persistence data.
+        
+        @param data persisted data to be parsed (string)
+        """
+        # TODO: implement this
--- a/Graphics/UMLClassDiagram.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/UMLClassDiagram.py	Sun Sep 09 14:46:59 2012 +0200
@@ -34,11 +34,14 @@
         @param name name of the view widget (string)
         @keyparam noAttrs flag indicating, that no attributes should be shown (boolean)
         """
-        UMLDialog.__init__(self, buildFunction=self.__buildClasses, parent=parent)
+        UMLDialog.__init__(self, "UMLClassDiagram", buildFunction=self.__buildClasses,
+            parent=parent)
         
         self.file = file
         self.noAttrs = noAttrs
         
+        self.umlView.setPersistenceData("file={0}".format(file))
+        
         pname = project.getProjectName()
         if pname and project.isProjectSource(self.file):
             name = self.trUtf8("Class Diagram {0}: {1}").format(
@@ -231,6 +234,7 @@
             name = "{0} (Module)".format(name)
         cl = ClassModel(name, meths[:], attrs[:])
         cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene)
+        cw.setId(self.umlView.getItemId())
         self.allClasses[className] = cw
         if _class.name not in self.allModules[self.file]:
             self.allModules[self.file].append(_class.name)
@@ -248,6 +252,7 @@
         """
         cl = ClassModel(_class)
         cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene)
+        cw.setId(self.umlView.getItemId())
         self.allClasses[_class] = cw
         if _class not in self.allModules[self.file]:
             self.allModules[self.file].append(_class)
--- a/Graphics/UMLDialog.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/UMLDialog.py	Sun Sep 09 14:46:59 2012 +0200
@@ -20,12 +20,14 @@
     """
     Class implementing a dialog showing UML like diagrams.
     """
-    def __init__(self, buildFunction=None, diagramName="Unnamed", parent=None, name=None):
+    def __init__(self, diagramType, diagramName="Unnamed", buildFunction=None,
+                 parent=None, name=""):
         """
         Constructor
         
+        @param diagramType type of the diagram (string)
+        @param diagramName name of the diagram (string)
         @param buildFunction function to build the diagram contents (function)
-        @param diagramName name of the diagram (string)
         @param parent parent widget of the view (QWidget)
         @param name name of the view widget (string)
         """
@@ -38,7 +40,8 @@
         
         self.buildFunction = buildFunction
         self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
-        self.umlView = UMLGraphicsView(self.scene, diagramName, self, "umlView")
+        self.umlView = UMLGraphicsView(self.scene, diagramType, diagramName,
+            self, "umlView")
         
         self.closeAct = \
             QAction(UI.PixmapCache.getIcon("close.png"),
--- a/Graphics/UMLGraphicsView.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/UMLGraphicsView.py	Sun Sep 09 14:46:59 2012 +0200
@@ -15,6 +15,7 @@
 from E5Gui import E5MessageBox, E5FileDialog
 
 from .UMLItem import UMLItem
+from .AssociationItem import AssociationItem
 from .UMLSceneSizeDialog import UMLSceneSizeDialog
 from .ZoomDialog import ZoomDialog
 
@@ -33,11 +34,13 @@
     """
     relayout = pyqtSignal()
     
-    def __init__(self, scene, diagramName="Unnamed", parent=None, name=None):
+    def __init__(self, scene, diagramType, diagramName="Unnamed", parent=None,
+                 name=None):
         """
         Constructor
         
         @param scene reference to the scene object (QGraphicsScene)
+        @param diagramType type of the diagram (string)
         @param diagramName name of the diagram (string)
         @param parent parent widget of the view (QWidget)
         @param name name of the view widget (string)
@@ -48,6 +51,11 @@
         self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
         
         self.diagramName = diagramName
+        self.diagramType = diagramType
+        
+        self.persistenceData = ""
+        self.__fileName = ""
+        self.__itemId = -1
         
         self.border = 10
         self.deltaSize = 100.0
@@ -72,8 +80,18 @@
         
         self.saveAct = \
             QAction(UI.PixmapCache.getIcon("fileSave.png"),
+                    self.trUtf8("Save"), self)
+        self.saveAct.triggered[()].connect(self.__save)
+        
+        self.saveAsAct = \
+            QAction(UI.PixmapCache.getIcon("fileSaveAs.png"),
+                    self.trUtf8("Save As..."), self)
+        self.saveAsAct.triggered[()].connect(self.__saveAs)
+        
+        self.saveImageAct = \
+            QAction(UI.PixmapCache.getIcon("fileSavePixmap.png"),
                     self.trUtf8("Save as PNG"), self)
-        self.saveAct.triggered[()].connect(self.__saveImage)
+        self.saveImageAct.triggered[()].connect(self.__saveImage)
         
         self.printAct = \
             QAction(UI.PixmapCache.getIcon("print.png"),
@@ -225,6 +243,8 @@
         toolBar.addAction(self.deleteShapeAct)
         toolBar.addSeparator()
         toolBar.addAction(self.saveAct)
+        toolBar.addAction(self.saveAsAct)
+        toolBar.addAction(self.saveImageAct)
         toolBar.addSeparator()
         toolBar.addAction(self.printPreviewAct)
         toolBar.addAction(self.printAct)
@@ -251,15 +271,16 @@
         
         return toolBar
         
-    def filteredItems(self, items):
+    def filteredItems(self, items, itemType=UMLItem):
         """
         Public method to filter a list of items.
         
         @param items list of items as returned by the scene object
             (QGraphicsItem)
+        @param itemType type to be filtered (class)
         @return list of interesting collision items (QGraphicsItem)
         """
-        return [itm for itm in items if isinstance(itm, UMLItem)]
+        return [itm for itm in items if isinstance(itm, itemType)]
         
     def selectItems(self, items):
         """
@@ -383,6 +404,7 @@
         """
         Private method to handle the re-layout context menu entry.
         """
+        self.__itemId = -1
         self.scene().clear()
         self.relayout.emit()
         
@@ -569,3 +591,109 @@
             else:
                 self.setZoom(pinch.scaleFactor())
             evt.accept()
+    
+    def setPersistenceData(self, data):
+        """
+        Public method to set additional persistence data.
+        
+        @param data string of additional data to be made persistent (string)
+        """
+        self.persistenceData = data
+    
+    def getPersistenceData(self):
+        """
+        Public method to get the additional persistence data.
+        
+        @return additional persistence data (string)
+        """
+        return self.persistenceData
+    
+    def getItemId(self):
+        """
+        Public method to get the ID to be assigned to an item.
+        
+        @return item ID (integer)
+        """
+        self.__itemId += 1
+        return self.__itemId
+
+    def findItem(self, id):
+        """
+        Public method to find an UML item based on the ID.
+        
+        @param id of the item to search for (integer)
+        @return item found (UMLItem) or None
+        """
+        for item in self.scene().items():
+            try:
+                itemID = item.getId()
+            except AttributeError:
+                continue
+            
+            if itemID == id:
+                return item
+        
+        return None
+    
+    def __save(self):
+        """
+        Private slot to save the diagram with the current name.
+        """
+        self.__saveAs(self.__fileName)
+    
+    def __saveAs(self, filename=""):
+        """
+        Private slot to save the diagram.
+        
+        @param filename name of the file to write to (string)
+        """
+        if not filename:
+            fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
+                self,
+                self.trUtf8("Save Diagram"),
+                "",
+                self.trUtf8("Eric5 Graphics File (*.e5g);;All Files (*)"),
+                "",
+                E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
+            if not fname:
+                return
+            ext = QFileInfo(fname).suffix()
+            if not ext:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fname += ex
+            if QFileInfo(fname).exists():
+                res = E5MessageBox.yesNo(self,
+                    self.trUtf8("Save Diagram"),
+                    self.trUtf8("<p>The file <b>{0}</b> already exists."
+                                " Overwrite it?</p>").format(fname),
+                    icon=E5MessageBox.Warning)
+                if not res:
+                    return
+            filename = fname
+        
+        lines = [
+            "version: 1.0",
+            "diagram_type: {0}".format(self.diagramType),
+            "diagram_name: {0}".format(self.diagramName),
+            "scene_size: {0};{1}".format(self.scene().width(), self.scene().height()),
+        ]
+        if self.persistenceData:
+            lines.append("diagram_data: {0}".format(self.persistenceData))
+        for item in self.filteredItems(self.scene().items(), UMLItem):
+            lines.append("item: id={0}, x={1}, y={2}{3}".format(
+                item.getId(), item.x(), item.y(), item.buildItemDataString()))
+        for item in self.filteredItems(self.scene().items(), AssociationItem):
+            lines.append("association: {0}".format(item.buildAssociationItemDataString()))
+        
+        try:
+            f = open(filename, "w", encoding="utf-8")
+            f.write("\n".join(lines))
+            f.close()
+        except (IOError, OSError) as err:
+            E5MessageBox.critical(self,
+                self.trUtf8("Save Diagram"),
+                self.trUtf8("""<p>The file <b>{0}</b> could not be saved.</p>"""
+                             """<p>Reason: {1}</p>""").format(fname, str(err)))
+        
+        self.__fileName = filename
--- a/Graphics/UMLItem.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Graphics/UMLItem.py	Sun Sep 09 14:46:59 2012 +0200
@@ -31,6 +31,7 @@
         self.margin = 5
         self.associations = []
         self.shouldAdjustAssociations = False
+        self.__id = -1
         
         self.setRect(x, y, 60, 30)
         
@@ -144,3 +145,39 @@
         painter.setBrush(self.brush())
         painter.drawRect(self.rect())
         self.adjustAssociations()
+        
+    def setId(self, id):
+        """
+        Public method to assign an ID to the item.
+        
+        @param id assigned ID (integer)
+        """
+        self.__id = id
+    
+    def getId(self):
+        """
+        Public method to get the item ID.
+        
+        @return ID of the item (integer)
+        """
+        return self.__id
+    
+    def buildItemDataString(self):
+        """
+        Public method to build a string to persist the specific item data.
+        
+        This string must start with ", " and should be built like
+        "attribute=value" with pairs separated by ", ". value must not contain ", "
+        or newlines.
+        
+        @return persistence data (string)
+        """
+        return ""
+    
+    def parseItemDataString(self, data):
+        """
+        Public method to parse the given persistence data.
+        
+        @param data persisted data to be parsed (string)
+        """
+        pass
--- a/Utilities/__init__.py	Sat Sep 08 18:20:46 2012 +0200
+++ b/Utilities/__init__.py	Sun Sep 09 14:46:59 2012 +0200
@@ -1678,6 +1678,21 @@
     
     return True
 
+
+def toBool(dataStr):
+    """
+    Module function to convert a string to a boolean value.
+    
+    @param dataStr string to be converted (string)
+    @return converted boolean value (boolean)
+    """
+    if dataStr in [ "True", "true", "1", "Yes", "yes"]:
+        return True
+    elif dataStr in ["False", "false", "0", "No", "no"]:
+        return False
+    else:
+        return bool(dataStr)
+
 ################################################################################
 # posix compatibility functions below
 ################################################################################
Binary file icons/default/fileSavePixmap.png has changed

eric ide

mercurial