UML Diagrams

Wed, 05 May 2021 18:17:24 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 05 May 2021 18:17:24 +0200
changeset 8291
3d79b1e5bf3c
parent 8290
6970128e3d71
child 8292
54b48d756b0a

UML Diagrams
- added code to save diagrams as JSON files

docs/changelog file | annotate | diff | comparison | revisions
eric6/Graphics/ApplicationDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/AssociationItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/ClassItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/ImportsDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/ModuleItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/PackageDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/PackageItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLClassDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLDialog.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLGraphicsView.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLItem.py file | annotate | diff | comparison | revisions
--- a/docs/changelog	Wed May 05 17:29:41 2021 +0200
+++ b/docs/changelog	Wed May 05 18:17:24 2021 +0200
@@ -11,6 +11,7 @@
      the project others browser
   -- improved the diagram layout of the Import Diagram and the
      Application Diagram
+  -- added code to save diagrams as JSON files
 
 Version 21.5:
 - bug fixes
--- a/eric6/Graphics/ApplicationDiagramBuilder.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/ApplicationDiagramBuilder.py	Wed May 05 18:17:24 2021 +0200
@@ -434,3 +434,15 @@
         self.initialize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {
+            "project_name": self.project.getProjectName(),
+            "no_modules": self.noModules,
+        }
--- a/eric6/Graphics/AssociationItem.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/AssociationItem.py	Wed May 05 18:17:24 2021 +0200
@@ -602,3 +602,17 @@
                     topToBottom = Utilities.toBool(value)
         
         return src, dst, assocType, topToBottom
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {
+            "src": self.itemA.getId(),
+            "dst": self.itemB.getId(),
+            "type": self.assocType.value,
+            "topToBottom": self.topToBottom,
+        }
--- a/eric6/Graphics/ClassItem.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/ClassItem.py	Wed May 05 18:17:24 2021 +0200
@@ -389,3 +389,19 @@
         self.__calculateSize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {
+            "is_external": self.external,
+            "no_attributes": self.noAttrs,
+            "name": self.model.getName(),
+            "attributes": self.model.getInstanceAttributes(),
+            "methods": self.model.getMethods(),
+            "class_attributes": self.model.getClassAttributes(),
+        }
--- a/eric6/Graphics/ImportsDiagramBuilder.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/ImportsDiagramBuilder.py	Wed May 05 18:17:24 2021 +0200
@@ -50,6 +50,12 @@
         
         self.showExternalImports = showExternalImports
         self.packagePath = os.path.abspath(package)
+        
+        self.__relPackagePath = (
+            self.project.getRelativePath(self.packagePath)
+            if self.project.isProjectSource(self.packagePath) else
+            ""
+        )
     
     def initialize(self):
         """
@@ -358,3 +364,23 @@
         self.initialize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        data = {
+            "project_name": self.project.getProjectName(),
+            "show_external": self.showExternalImports,
+        }
+        
+        data["package"] = (
+            Utilities.fromNativeSeparators(self.__relPackagePath)
+            if self.__relPackagePath else
+            Utilities.fromNativeSeparators(self.packagePath)
+        )
+        
+        return data
--- a/eric6/Graphics/ModuleItem.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/ModuleItem.py	Wed May 05 18:17:24 2021 +0200
@@ -227,3 +227,15 @@
         self.__calculateSize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {
+            "name": self.model.getName(),
+            "classes": self.model.getClasses(),
+        }
--- a/eric6/Graphics/PackageDiagramBuilder.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/PackageDiagramBuilder.py	Wed May 05 18:17:24 2021 +0200
@@ -45,6 +45,12 @@
         
         self.package = os.path.abspath(package)
         self.noAttrs = noAttrs
+        
+        self.__relPackage = (
+            self.project.getRelativePath(self.package)
+            if self.project.isProjectSource(self.package) else
+            ""
+        )
     
     def initialize(self):
         """
@@ -502,3 +508,23 @@
         self.initialize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        data = {
+            "project_name": self.project.getProjectName(),
+            "no_attributes": self.noAttrs,
+        }
+        
+        data["package"] = (
+            Utilities.fromNativeSeparators(self.__relPackage)
+            if self.__relPackage else
+            Utilities.fromNativeSeparators(self.package)
+        )
+        
+        return data
--- a/eric6/Graphics/PackageItem.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/PackageItem.py	Wed May 05 18:17:24 2021 +0200
@@ -256,3 +256,16 @@
         self.__calculateSize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {
+            "name": self.model.getName(),
+            "no_nodules": self.noModules,
+            "modules": self.model.getModules(),
+        }
--- a/eric6/Graphics/UMLClassDiagramBuilder.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/UMLClassDiagramBuilder.py	Wed May 05 18:17:24 2021 +0200
@@ -41,6 +41,12 @@
         
         self.file = file
         self.noAttrs = noAttrs
+        
+        self.__relFile = (
+            self.project.getRelativePath(self.file)
+            if self.project.isProjectSource(self.file) else
+            ""
+        )
     
     def initialize(self):
         """
@@ -339,3 +345,23 @@
         self.initialize()
         
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        data = {
+            "project_name": self.project.getProjectName(),
+            "no_attributes": self.noAttrs,
+        }
+        
+        data["file"] = (
+            Utilities.fromNativeSeparators(self.__relFile)
+            if self.__relFile else
+            Utilities.fromNativeSeparators(self.file)
+        )
+        
+        return data
--- a/eric6/Graphics/UMLDiagramBuilder.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/UMLDiagramBuilder.py	Wed May 05 18:17:24 2021 +0200
@@ -70,3 +70,12 @@
         @rtype bool
         """
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {}
--- a/eric6/Graphics/UMLDialog.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/UMLDialog.py	Wed May 05 18:17:24 2021 +0200
@@ -8,8 +8,9 @@
 """
 
 import enum
+import json
 
-from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo
+from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo, QCoreApplication
 from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene
 
 from E5Gui import E5MessageBox, E5FileDialog
@@ -36,6 +37,17 @@
     """
     FileVersions = ("1.0")
     
+    UMLDialogType2String = {
+        UMLDialogType.CLASS_DIAGRAM:
+            QCoreApplication.translate("UMLDialog", "Class Diagram"),
+        UMLDialogType.PACKAGE_DIAGRAM:
+            QCoreApplication.translate("UMLDialog", "Package Diagram"),
+        UMLDialogType.IMPORTS_DIAGRAM:
+            QCoreApplication.translate("UMLDialog", "Imports Diagram"),
+        UMLDialogType.APPLICATION_DIAGRAM:
+            QCoreApplication.translate("UMLDialog", "Application Diagram"),
+    }
+    
     def __init__(self, diagramType, project, path="", parent=None,
                  initBuilder=True, **kwargs):
         """
@@ -60,12 +72,6 @@
         
         self.__project = project
         self.__diagramType = diagramType
-        self.__diagramTypeString = {
-            UMLDialogType.CLASS_DIAGRAM: "Class Diagram",
-            UMLDialogType.PACKAGE_DIAGRAM: "Package Diagram",
-            UMLDialogType.IMPORTS_DIAGRAM: "Imports Diagram",
-            UMLDialogType.APPLICATION_DIAGRAM: "Application Diagram",
-        }.get(diagramType, "Illegal Diagram Type")
         
         from .UMLGraphicsView import UMLGraphicsView
         self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
@@ -84,7 +90,20 @@
         
         self.umlView.relayout.connect(self.__relayout)
         
-        self.setWindowTitle(self.__diagramTypeString)
+        self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
+    
+    def __getDiagramTitel(self, diagramType):
+        """
+        Private method to get a textual description for the diagram type.
+        
+        @param diagramType diagram type string
+        @type str
+        @return titel of the diagram
+        @rtype str
+        """
+        return UMLDialog.UMLDialogType2String.get(
+            diagramType, self.tr("Illegal Diagram Type")
+        )
     
     def __initActions(self):
         """
@@ -208,7 +227,6 @@
         """
         self.__saveAs(self.__fileName)
     
-    # TODO: change this to save file in JSON format
     @pyqtSlot()
     def __saveAs(self, filename=""):
         """
@@ -222,7 +240,9 @@
                 self,
                 self.tr("Save Diagram"),
                 "",
-                self.tr("Eric Graphics File (*.e5g);;All Files (*)"),
+                self.tr("Eric Graphics File (*.egj);;"
+                        "Eric Text Graphics File (*.e5g);;"
+                        "All Files (*)"),
                 "",
                 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
             if not fname:
@@ -243,31 +263,16 @@
                     return
             filename = fname
         
-        lines = [
-            "version: 1.0",
-            "diagram_type: {0} ({1})".format(
-                self.__diagramType.value, self.__diagramTypeString),
-            "scene_size: {0};{1}".format(self.scene.width(),
-                                         self.scene.height()),
-        ]
-        persistenceData = self.builder.getPersistenceData()
-        if persistenceData:
-            lines.append("builder_data: {0}".format(persistenceData))
-        lines.extend(self.umlView.getPersistenceData())
+        res = (
+            self.__writeLineBasedGraphicsFile(filename)
+            if filename.endswith(".e5g") else
+            # JSON format is the default
+            self.__writeJsonGraphicsFile(filename)
+        )
         
-        try:
-            with open(filename, "w", encoding="utf-8") as f:
-                f.write("\n".join(lines))
-        except OSError as err:
-            E5MessageBox.critical(
-                self,
-                self.tr("Save Diagram"),
-                self.tr(
-                    """<p>The file <b>{0}</b> could not be saved.</p>"""
-                    """<p>Reason: {1}</p>""").format(filename, str(err)))
-            return
-        
-        self.__fileName = filename
+        if res:
+            # save the file name only in case of success
+            self.__fileName = filename
     
     # TODO: add loading of file in JSON format
     # TODO: eric7: delete the current one
@@ -290,6 +295,26 @@
                 # Canceled by user
                 return False
         
+        if filename.endswith(".e5g"):
+            return self.__readLineBasedGraphicsFile(filename)
+        else:
+            return False
+    
+    #######################################################################
+    ## Methods to read and write eric graphics files of the old line
+    ## based file format.
+    #######################################################################
+    
+    def __readLineBasedGraphicsFile(self, filename):
+        """
+        Private method to read an eric graphics file using the old line
+        based file format.
+        
+        @param filename name of the file to be read
+        @type str
+        @return flag indicating success
+        @rtype bool
+        """
         try:
             with open(filename, "r", encoding="utf-8") as f:
                 data = f.read()
@@ -327,10 +352,8 @@
                 self.__showInvalidDataMessage(filename, linenum)
                 return False
             try:
-                diagramType, diagramTypeString = value.strip().split(None, 1)
+                diagramType = value.strip().split(None, 1)[0]
                 self.__diagramType = UMLDialogType(int(diagramType))
-                self.__diagramTypeString = diagramTypeString[1:-1]
-                # remove opening and closing bracket
             except ValueError:
                 self.__showInvalidDataMessage(filename, linenum)
                 return False
@@ -373,11 +396,47 @@
         
         # everything worked fine, so remember the file name and set the
         # window title
-        self.setWindowTitle(self.__diagramTypeString)
+        self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
         self.__fileName = filename
         
         return True
     
+    def __writeLineBasedGraphicsFile(self, filename):
+        """
+        Private method to write an eric graphics file using the old line
+        based file format.
+        
+        @param filename name of the file to write to
+        @type str
+        @return flag indicating a successful write
+        @rtype bool
+        """
+        lines = [
+            "version: 1.0",
+            "diagram_type: {0} ({1})".format(
+                self.__diagramType.value,
+                self.__getDiagramTitel(self.__diagramType)),
+            "scene_size: {0};{1}".format(self.scene.width(),
+                                         self.scene.height()),
+        ]
+        persistenceData = self.builder.getPersistenceData()
+        if persistenceData:
+            lines.append("builder_data: {0}".format(persistenceData))
+        lines.extend(self.umlView.getPersistenceData())
+        
+        try:
+            with open(filename, "w", encoding="utf-8") as f:
+                f.write("\n".join(lines))
+            return True
+        except OSError as err:
+            E5MessageBox.critical(
+                self,
+                self.tr("Save Diagram"),
+                self.tr(
+                    """<p>The file <b>{0}</b> could not be saved.</p>"""
+                    """<p>Reason: {1}</p>""").format(filename, str(err)))
+            return False
+    
     def __showInvalidDataMessage(self, filename, linenum=-1):
         """
         Private slot to show a message dialog indicating an invalid data file.
@@ -396,3 +455,42 @@
                     ).format(filename, linenum + 1)
         )
         E5MessageBox.critical(self, self.tr("Load Diagram"), msg)
+    
+    #######################################################################
+    ## Methods to read and write eric graphics files of the JSON based
+    ## file format.
+    #######################################################################
+    
+    def __writeJsonGraphicsFile(self, filename):
+        """
+        Private method to write an eric graphics file using the JSON based
+        file format.
+        
+        @param filename name of the file to write to
+        @type str
+        @return flag indicating a successful write
+        @rtype bool
+        """
+        data = {
+            "version": "1.0",
+            "type": self.__diagramType.value,
+            "title": self.__getDiagramTitel(self.__diagramType),
+            "width": self.scene.width(),
+            "height": self.scene.height(),
+            "builder": self.builder.toDict(),
+            "view": self.umlView.toDict(),
+        }
+        
+        try:
+            jsonString = json.dumps(data, indent=2)
+            with open(filename, "w") as f:
+                f.write(jsonString)
+            return True
+        except (TypeError, OSError) as err:
+            E5MessageBox.critical(
+                self,
+                self.tr("Save Diagram"),
+                self.tr(
+                    """<p>The file <b>{0}</b> could not be saved.</p>"""
+                    """<p>Reason: {1}</p>""").format(filename, str(err)))
+            return False
--- a/eric6/Graphics/UMLGraphicsView.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/UMLGraphicsView.py	Wed May 05 18:17:24 2021 +0200
@@ -806,3 +806,35 @@
                 self.scene().addItem(assoc)
         
         return True, -1
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        items = []
+        for item in self.filteredItems(self.scene().items(), UMLItem):
+            items.append({
+                "id": item.getId(),
+                "x": item.x(),
+                "y": item.y(),
+                "type": item.getItemType(),
+                "data": item.toDict()
+            })
+        
+        from .AssociationItem import AssociationItem
+        associations = [
+            assoc.toDict()
+            for assoc in self.filteredItems(self.scene().items(),
+                                            AssociationItem)
+        ]
+        
+        data = {
+            "diagram_name": self.diagramName,
+            "items": items,
+            "associations": associations,
+        }
+        
+        return data
--- a/eric6/Graphics/UMLItem.py	Wed May 05 17:29:41 2021 +0200
+++ b/eric6/Graphics/UMLItem.py	Wed May 05 18:17:24 2021 +0200
@@ -276,3 +276,12 @@
         @rtype bool
         """
         return True
+    
+    def toDict(self):
+        """
+        Public method to collect data to be persisted.
+        
+        @return dictionary containing data to be persisted
+        @rtype dict
+        """
+        return {}

eric ide

mercurial