src/eric7/Graphics/UMLDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9153
506e35e424d5
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Graphics/UMLDialog.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,541 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2007 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog showing UML like diagrams.
+"""
+
+import enum
+import json
+import pathlib
+
+from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication
+from PyQt6.QtGui import QAction
+from PyQt6.QtWidgets import QToolBar, QGraphicsScene
+
+from EricWidgets import EricMessageBox, EricFileDialog
+from EricWidgets.EricMainWindow import EricMainWindow
+
+import UI.Config
+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(EricMainWindow):
+    """
+    Class implementing a dialog showing UML like diagrams.
+    """
+    FileVersions = ("1.0", )
+    JsonFileVersions = ("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
+        @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
+        @type bool
+        @keyparam kwargs diagram specific data
+        @type dict
+        """
+        super().__init__(parent)
+        self.setObjectName("UMLDialog")
+        
+        self.__project = project
+        self.__diagramType = diagramType
+        
+        from .UMLGraphicsView import UMLGraphicsView
+        self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
+        self.umlView = UMLGraphicsView(self.scene, parent=self)
+        self.builder = self.__diagramBuilder(
+            self.__diagramType, path, **kwargs)
+        if self.builder and initBuilder:
+            self.builder.initialize()
+        
+        self.__fileName = ""
+        
+        self.__initActions()
+        self.__initToolBars()
+        
+        self.setCentralWidget(self.umlView)
+        
+        self.umlView.relayout.connect(self.__relayout)
+        
+        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):
+        """
+        Private slot to initialize the actions.
+        """
+        self.closeAct = QAction(
+            UI.PixmapCache.getIcon("close"),
+            self.tr("Close"), self)
+        self.closeAct.triggered.connect(self.close)
+        
+        self.openAct = QAction(
+            UI.PixmapCache.getIcon("open"),
+            self.tr("Load"), self)
+        self.openAct.triggered.connect(self.load)
+        
+        self.saveAct = QAction(
+            UI.PixmapCache.getIcon("fileSave"),
+            self.tr("Save"), self)
+        self.saveAct.triggered.connect(self.__save)
+        
+        self.saveAsAct = QAction(
+            UI.PixmapCache.getIcon("fileSaveAs"),
+            self.tr("Save As..."), self)
+        self.saveAsAct.triggered.connect(self.__saveAs)
+        
+        self.saveImageAct = QAction(
+            UI.PixmapCache.getIcon("fileSavePixmap"),
+            self.tr("Save as Image"), self)
+        self.saveImageAct.triggered.connect(self.umlView.saveImage)
+        
+        self.printAct = QAction(
+            UI.PixmapCache.getIcon("print"),
+            self.tr("Print"), self)
+        self.printAct.triggered.connect(self.umlView.printDiagram)
+        
+        self.printPreviewAct = QAction(
+            UI.PixmapCache.getIcon("printPreview"),
+            self.tr("Print Preview"), self)
+        self.printPreviewAct.triggered.connect(
+            self.umlView.printPreviewDiagram)
+    
+    def __initToolBars(self):
+        """
+        Private slot to initialize the toolbars.
+        """
+        self.windowToolBar = QToolBar(self.tr("Window"), self)
+        self.windowToolBar.setIconSize(UI.Config.ToolBarIconSize)
+        self.windowToolBar.addAction(self.closeAct)
+        
+        self.fileToolBar = QToolBar(self.tr("File"), self)
+        self.fileToolBar.setIconSize(UI.Config.ToolBarIconSize)
+        self.fileToolBar.addAction(self.openAct)
+        self.fileToolBar.addSeparator()
+        self.fileToolBar.addAction(self.saveAct)
+        self.fileToolBar.addAction(self.saveAsAct)
+        self.fileToolBar.addAction(self.saveImageAct)
+        self.fileToolBar.addSeparator()
+        self.fileToolBar.addAction(self.printPreviewAct)
+        self.fileToolBar.addAction(self.printAct)
+        
+        self.umlToolBar = self.umlView.initToolBar()
+        
+        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolBar)
+        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.windowToolBar)
+        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.umlToolBar)
+    
+    def show(self, fromFile=False):
+        """
+        Public method to show the dialog.
+        
+        @param fromFile flag indicating, that the diagram was loaded
+            from file
+        @type bool
+        """
+        if not fromFile and self.builder:
+            self.builder.buildDiagram()
+        super().show()
+    
+    def __relayout(self):
+        """
+        Private method to re-layout the diagram.
+        """
+        if self.builder:
+            self.builder.buildDiagram()
+    
+    def __diagramBuilder(self, diagramType, path, **kwargs):
+        """
+        Private method to instantiate a diagram builder object.
+        
+        @param diagramType type of the diagram
+        @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
+        @rtype UMLDiagramBuilder
+        """
+        if diagramType == UMLDialogType.CLASS_DIAGRAM:
+            from .UMLClassDiagramBuilder import UMLClassDiagramBuilder
+            return UMLClassDiagramBuilder(
+                self, self.umlView, self.__project, path, **kwargs)
+        elif diagramType == UMLDialogType.PACKAGE_DIAGRAM:
+            from .PackageDiagramBuilder import PackageDiagramBuilder
+            return PackageDiagramBuilder(
+                self, self.umlView, self.__project, path, **kwargs)
+        elif diagramType == UMLDialogType.IMPORTS_DIAGRAM:
+            from .ImportsDiagramBuilder import ImportsDiagramBuilder
+            return ImportsDiagramBuilder(
+                self, self.umlView, self.__project, path, **kwargs)
+        elif diagramType == UMLDialogType.APPLICATION_DIAGRAM:
+            from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
+            return ApplicationDiagramBuilder(
+                self, self.umlView, self.__project, **kwargs)
+        else:
+            return None
+    
+    def __save(self):
+        """
+        Private slot to save the diagram with the current name.
+        """
+        self.__saveAs(self.__fileName)
+    
+    @pyqtSlot()
+    def __saveAs(self, filename=""):
+        """
+        Private slot to save the diagram.
+        
+        @param filename name of the file to write to
+        @type str
+        """
+        if not filename:
+            fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
+                self,
+                self.tr("Save Diagram"),
+                "",
+                self.tr("Eric Graphics File (*.egj);;"
+                        "Eric Text Graphics File (*.e5g);;"
+                        "All Files (*)"),
+                "",
+                EricFileDialog.DontConfirmOverwrite)
+            if not fname:
+                return
+            
+            fpath = pathlib.Path(fname)
+            if not fpath.suffix:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fpath = fpath.with_suffix(ex)
+            if fpath.exists():
+                res = EricMessageBox.yesNo(
+                    self,
+                    self.tr("Save Diagram"),
+                    self.tr("<p>The file <b>{0}</b> already exists."
+                            " Overwrite it?</p>").format(fpath),
+                    icon=EricMessageBox.Warning)
+                if not res:
+                    return
+            filename = str(fpath)
+        
+        res = self.__writeJsonGraphicsFile(filename)
+        
+        if res:
+            # save the file name only in case of success
+            self.__fileName = filename
+    
+    # Note: remove loading of eric6 line based diagram format after 22.6
+    def load(self, filename=""):
+        """
+        Public method to load a diagram from a file.
+        
+        @param filename name of the file to be loaded
+        @type str
+        @return flag indicating success
+        @rtype bool
+        """
+        if not filename:
+            filename = EricFileDialog.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()
+        except OSError as err:
+            EricMessageBox.critical(
+                self,
+                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
+        
+        lines = data.splitlines()
+        if len(lines) < 3:
+            self.__showInvalidDataMessage(filename)
+            return False
+        
+        try:
+            # step 1: check version
+            linenum = 0
+            key, value = lines[linenum].split(": ", 1)
+            if (
+                key.strip() != "version" or
+                value.strip() not in UMLDialog.FileVersions
+            ):
+                self.__showInvalidDataMessage(filename, linenum)
+                return False
+            else:
+                version = value
+            
+            # step 2: extract diagram type
+            linenum += 1
+            key, value = lines[linenum].split(": ", 1)
+            if key.strip() != "diagram_type":
+                self.__showInvalidDataMessage(filename, linenum)
+                return False
+            try:
+                diagramType = value.strip().split(None, 1)[0]
+                self.__diagramType = UMLDialogType(int(diagramType))
+            except ValueError:
+                self.__showInvalidDataMessage(filename, linenum)
+                return False
+            self.scene.clear()
+            self.builder = self.__diagramBuilder(self.__diagramType, "")
+            
+            # step 3: extract scene size
+            linenum += 1
+            key, value = lines[linenum].split(": ", 1)
+            if key.strip() != "scene_size":
+                self.__showInvalidDataMessage(filename, linenum)
+                return False
+            try:
+                width, height = [float(v.strip()) for v in value.split(";")]
+            except ValueError:
+                self.__showInvalidDataMessage(filename, linenum)
+                return False
+            self.umlView.setSceneSize(width, height)
+            
+            # step 4: extract builder data if available
+            linenum += 1
+            key, value = lines[linenum].split(": ", 1)
+            if key.strip() == "builder_data":
+                ok = self.builder.parsePersistenceData(version, value)
+                if not ok:
+                    self.__showInvalidDataMessage(filename, linenum)
+                    return False
+                linenum += 1
+            
+            # step 5: extract the graphics items
+            ok, vlinenum = self.umlView.parsePersistenceData(
+                version, lines[linenum:])
+            if not ok:
+                self.__showInvalidDataMessage(filename, linenum + vlinenum)
+                return False
+        
+        except IndexError:
+            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
+    
+    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
+        @type str
+        @param linenum number of the invalid line
+        @type int
+        """
+        msg = (
+            self.tr("""<p>The file <b>{0}</b> does not contain"""
+                    """ valid data.</p>""").format(filename)
+            if linenum < 0 else
+            self.tr("""<p>The file <b>{0}</b> does not contain"""
+                    """ valid data.</p><p>Invalid line: {1}</p>"""
+                    ).format(filename, linenum + 1)
+        )
+        EricMessageBox.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:
+            EricMessageBox.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:
+            EricMessageBox.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 = EricMessageBox.warning(
+                        self,
+                        self.tr("Load Diagram"),
+                        msg,
+                        EricMessageBox.Abort | EricMessageBox.Ignore,
+                        EricMessageBox.Abort)
+                    if res == EricMessageBox.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