eric6/Graphics/UMLDialog.py

branch
maintenance
changeset 8400
b3eefd7e58d1
parent 8273
698ae46f40a4
parent 8295
3f5e8b0a338e
diff -r 197414ba11cc -r b3eefd7e58d1 eric6/Graphics/UMLDialog.py
--- a/eric6/Graphics/UMLDialog.py	Sat May 01 14:27:38 2021 +0200
+++ b/eric6/Graphics/UMLDialog.py	Thu Jun 03 11:39:23 2021 +0200
@@ -7,7 +7,10 @@
 Module implementing a dialog showing UML like diagrams.
 """
 
-from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo
+import enum
+import json
+
+from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo, QCoreApplication
 from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene
 
 from E5Gui import E5MessageBox, E5FileDialog
@@ -17,44 +20,59 @@
 import UI.PixmapCache
 
 
+class UMLDialogType(enum.Enum):
+    """
+    Class defining the UML dialog types.
+    """
+    CLASS_DIAGRAM = 0
+    PACKAGE_DIAGRAM = 1
+    IMPORTS_DIAGRAM = 2
+    APPLICATION_DIAGRAM = 3
+    NO_DIAGRAM = 255
+
+
 class UMLDialog(E5MainWindow):
     """
     Class implementing a dialog showing UML like diagrams.
     """
-    # convert to Enum
-    NoDiagram = 255
-    ClassDiagram = 0
-    PackageDiagram = 1
-    ImportsDiagram = 2
-    ApplicationDiagram = 3
+    FileVersions = ("1.0", )
+    JsonFileVersions = ("1.0", )
     
-    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):
         """
         Constructor
         
-        @param diagramType type of the diagram (one of ApplicationDiagram,
-            ClassDiagram, ImportsDiagram, NoDiagram, PackageDiagram)
-        @param project reference to the project object (Project)
-        @param path file or directory path to build the diagram from (string)
-        @param parent parent widget of the dialog (QWidget)
+        @param diagramType type of the diagram
+        @type UMLDialogType
+        @param project reference to the project object
+        @type Project
+        @param path file or directory path to build the diagram from
+        @type str
+        @param parent parent widget of the dialog
+        @type QWidget
         @param initBuilder flag indicating to initialize the diagram
-            builder (boolean)
+            builder
+        @type bool
         @keyparam kwargs diagram specific data
+        @type dict
         """
         super().__init__(parent)
         self.setObjectName("UMLDialog")
         
         self.__project = project
         self.__diagramType = diagramType
-        self.__diagramTypeString = {
-            UMLDialog.ClassDiagram: "Class Diagram",
-            UMLDialog.PackageDiagram: "Package Diagram",
-            UMLDialog.ImportsDiagram: "Imports Diagram",
-            UMLDialog.ApplicationDiagram: "Application Diagram",
-        }.get(diagramType, "Illegal Diagram Type")
         
         from .UMLGraphicsView import UMLGraphicsView
         self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
@@ -73,7 +91,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):
         """
@@ -145,7 +176,8 @@
         Public method to show the dialog.
         
         @param fromFile flag indicating, that the diagram was loaded
-            from file (boolean)
+            from file
+        @type bool
         """
         if not fromFile and self.builder:
             self.builder.buildDiagram()
@@ -153,7 +185,7 @@
     
     def __relayout(self):
         """
-        Private method to relayout the diagram.
+        Private method to re-layout the diagram.
         """
         if self.builder:
             self.builder.buildDiagram()
@@ -163,34 +195,27 @@
         Private method to instantiate a diagram builder object.
         
         @param diagramType type of the diagram
-            (one of ApplicationDiagram, ClassDiagram, ImportsDiagram,
-            PackageDiagram)
-        @param path file or directory path to build the diagram from (string)
+        @type UMLDialogType
+        @param path file or directory path to build the diagram from
+        @type str
         @keyparam kwargs diagram specific data
+        @type dict
         @return reference to the instantiated diagram builder
-        @exception ValueError raised to indicate an illegal diagram type
+        @rtype UMLDiagramBuilder
         """
-        if diagramType not in (
-            UMLDialog.ClassDiagram, UMLDialog.PackageDiagram,
-            UMLDialog.ImportsDiagram, UMLDialog.ApplicationDiagram,
-            UMLDialog.NoDiagram
-        ):
-            raise ValueError(self.tr(
-                "Illegal diagram type '{0}' given.").format(diagramType))
-        
-        if diagramType == UMLDialog.ClassDiagram:
+        if diagramType == UMLDialogType.CLASS_DIAGRAM:
             from .UMLClassDiagramBuilder import UMLClassDiagramBuilder
             return UMLClassDiagramBuilder(
                 self, self.umlView, self.__project, path, **kwargs)
-        elif diagramType == UMLDialog.PackageDiagram:
+        elif diagramType == UMLDialogType.PACKAGE_DIAGRAM:
             from .PackageDiagramBuilder import PackageDiagramBuilder
             return PackageDiagramBuilder(
                 self, self.umlView, self.__project, path, **kwargs)
-        elif diagramType == UMLDialog.ImportsDiagram:
+        elif diagramType == UMLDialogType.IMPORTS_DIAGRAM:
             from .ImportsDiagramBuilder import ImportsDiagramBuilder
             return ImportsDiagramBuilder(
                 self, self.umlView, self.__project, path, **kwargs)
-        elif diagramType == UMLDialog.ApplicationDiagram:
+        elif diagramType == UMLDialogType.APPLICATION_DIAGRAM:
             from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
             return ApplicationDiagramBuilder(
                 self, self.umlView, self.__project, **kwargs)
@@ -203,20 +228,22 @@
         """
         self.__saveAs(self.__fileName)
     
-    # TODO: change this to save file in JSON format
     @pyqtSlot()
     def __saveAs(self, filename=""):
         """
         Private slot to save the diagram.
         
-        @param filename name of the file to write to (string)
+        @param filename name of the file to write to
+        @type str
         """
         if not filename:
             fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
                 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:
@@ -237,49 +264,61 @@
                     return
             filename = fname
         
-        lines = [
-            "version: 1.0",
-            "diagram_type: {0} ({1})".format(
-                self.__diagramType, 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: delete the current one with eric7
-    def load(self):
+    # TODO: eric7: delete the current one
+    def load(self, filename=""):
         """
         Public method to load a diagram from a file.
         
-        @return flag indicating success (boolean)
+        @param filename name of the file to be loaded
+        @type str
+        @return flag indicating success
+        @rtype bool
         """
-        filename = E5FileDialog.getOpenFileName(
-            self,
-            self.tr("Load Diagram"),
-            "",
-            self.tr("Eric Graphics File (*.e5g);;All Files (*)"))
         if not filename:
-            # Cancelled by user
-            return False
+            filename = E5FileDialog.getOpenFileName(
+                self,
+                self.tr("Load Diagram"),
+                "",
+                self.tr("Eric Graphics File (*.egj);;"
+                        "Eric Text Graphics File (*.e5g);;"
+                        "All Files (*)"))
+            if not filename:
+                # Canceled by user
+                return False
         
+        return (
+            self.__readLineBasedGraphicsFile(filename)
+            if filename.endswith(".e5g") else
+            # JSON format is the default
+            self.__readJsonGraphicsFile(filename)
+        )
+    
+    #######################################################################
+    ## 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()
@@ -317,10 +356,8 @@
                 self.__showInvalidDataMessage(filename, linenum)
                 return False
             try:
-                diagramType, diagramTypeString = value.strip().split(None, 1)
-                self.__diagramType = int(self.__diagramType)
-                self.__diagramTypeString = diagramTypeString[1:-1]
-                # remove opening an closing bracket
+                diagramType = value.strip().split(None, 1)[0]
+                self.__diagramType = UMLDialogType(int(diagramType))
             except ValueError:
                 self.__showInvalidDataMessage(filename, linenum)
                 return False
@@ -363,17 +400,55 @@
         
         # 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.
         
-        @param filename name of the file containing the invalid data (string)
-        @param linenum number of the invalid line (integer)
+        @param filename name of the file containing the invalid data
+        @type str
+        @param linenum number of the invalid line
+        @type int
         """
         msg = (
             self.tr("""<p>The file <b>{0}</b> does not contain"""
@@ -384,3 +459,123 @@
                     ).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
+    
+    def __readJsonGraphicsFile(self, filename):
+        """
+        Private method to read an eric graphics file using the JSON based
+        file format.
+        
+        @param filename name of the file to be read
+        @type str
+        @return flag indicating a successful read
+        @rtype bool
+        """
+        try:
+            with open(filename, "r") as f:
+                jsonString = f.read()
+            data = json.loads(jsonString)
+        except (OSError, json.JSONDecodeError) as err:
+            E5MessageBox.critical(
+                None,
+                self.tr("Load Diagram"),
+                self.tr(
+                    """<p>The file <b>{0}</b> could not be read.</p>"""
+                    """<p>Reason: {1}</p>""").format(filename, str(err))
+            )
+            return False
+        
+        try:
+            # step 1: check version
+            if data["version"] in UMLDialog.JsonFileVersions:
+                version = data["version"]
+            else:
+                self.__showInvalidDataMessage(filename)
+                return False
+            
+            # step 2: set diagram type
+            try:
+                self.__diagramType = UMLDialogType(data["type"])
+            except ValueError:
+                self.__showInvalidDataMessage(filename)
+                return False
+            self.scene.clear()
+            self.builder = self.__diagramBuilder(self.__diagramType, "")
+            
+            # step 3: set scene size
+            self.umlView.setSceneSize(data["width"], data["height"])
+            
+            # step 4: extract builder data if available
+            ok, msg = self.builder.fromDict(version, data["builder"])
+            if not ok:
+                if msg:
+                    res = E5MessageBox.warning(
+                        self,
+                        self.tr("Load Diagram"),
+                        msg,
+                        E5MessageBox.StandardButtons(
+                            E5MessageBox.Abort |
+                            E5MessageBox.Ignore),
+                        E5MessageBox.Abort)
+                    if res == E5MessageBox.Abort:
+                        return False
+                    else:
+                        self.umlView.setLayoutActionsEnabled(False)
+                else:
+                    self.__showInvalidDataMessage(filename)
+                    return False
+            
+            # step 5: extract the graphics items
+            ok = self.umlView.fromDict(version, data["view"])
+            if not ok:
+                self.__showInvalidDataMessage(filename)
+                return False
+        except KeyError:
+            self.__showInvalidDataMessage(filename)
+            return False
+        
+        # everything worked fine, so remember the file name and set the
+        # window title
+        self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
+        self.__fileName = filename
+        
+        return True

eric ide

mercurial