src/eric7/Graphics/UMLDialog.py

Sat, 26 Apr 2025 12:34:32 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 26 Apr 2025 12:34:32 +0200
branch
eric7
changeset 11240
c48c615c04a3
parent 11090
f5f5f5803935
permissions
-rw-r--r--

MicroPython
- Added a configuration option to disable the support for the no longer produced Pimoroni Pico Wireless Pack.

# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog showing UML like diagrams.
"""

import enum
import json
import pathlib

from PyQt6.QtCore import QCoreApplication, Qt, pyqtSlot
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QGraphicsScene, QToolBar

from eric7.EricGui import EricPixmapCache
from eric7.EricWidgets import EricFileDialog, EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp
from eric7.EricWidgets.EricMainWindow import EricMainWindow
from eric7.RemoteServerInterface import EricServerFileDialog
from eric7.SystemUtilities import FileSystemUtilities

from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
from .ImportsDiagramBuilder import ImportsDiagramBuilder
from .PackageDiagramBuilder import PackageDiagramBuilder
from .UMLClassDiagramBuilder import UMLClassDiagramBuilder
from .UMLGraphicsView import UMLGraphicsView


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
        self.__remotefsInterface = (
            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
        )

        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(
            EricPixmapCache.getIcon("close"), self.tr("Close"), self
        )
        self.closeAct.triggered.connect(self.close)

        self.openAct = QAction(EricPixmapCache.getIcon("open"), self.tr("Load"), self)
        self.openAct.triggered.connect(self.load)

        self.saveAct = QAction(
            EricPixmapCache.getIcon("fileSave"), self.tr("Save"), self
        )
        self.saveAct.triggered.connect(self.__save)

        self.saveAsAct = QAction(
            EricPixmapCache.getIcon("fileSaveAs"), self.tr("Save As..."), self
        )
        self.saveAsAct.triggered.connect(self.__saveAs)

        self.saveImageAct = QAction(
            EricPixmapCache.getIcon("fileSavePixmap"), self.tr("Save as Image"), self
        )
        self.saveImageAct.triggered.connect(self.umlView.saveImage)

        self.printAct = QAction(
            EricPixmapCache.getIcon("print"), self.tr("Print"), self
        )
        self.printAct.triggered.connect(self.umlView.printDiagram)

        self.printPreviewAct = QAction(
            EricPixmapCache.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.addAction(self.closeAct)

        self.fileToolBar = QToolBar(self.tr("File"), self)
        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:
            return UMLClassDiagramBuilder(
                self, self.umlView, self.__project, path, **kwargs
            )
        elif diagramType == UMLDialogType.PACKAGE_DIAGRAM:
            return PackageDiagramBuilder(
                self, self.umlView, self.__project, path, **kwargs
            )
        elif diagramType == UMLDialogType.IMPORTS_DIAGRAM:
            return ImportsDiagramBuilder(
                self, self.umlView, self.__project, path, **kwargs
            )
        elif diagramType == UMLDialogType.APPLICATION_DIAGRAM:
            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:
            if FileSystemUtilities.isRemoteFileName(self.__project.getProjectPath()):
                fname, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter(
                    self,
                    self.tr("Save Diagram"),
                    self.__project.getProjectPath(),
                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
                    "",
                )
                if not fname:
                    return

                ext = self.__remotefsInterface.splitext(fname)[1]
                if not ext:
                    ex = selectedFilter.split("(*")[1].split(")")[0]
                    if ex:
                        fname += ex
                filename = fname
                fileExists = self.__remotefsInterface.exists(filename)
            else:
                fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
                    self,
                    self.tr("Save Diagram"),
                    self.__project.getProjectPath(),
                    self.tr("Eric Graphics File (*.egj);;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)
                fileExists = fpath.exists()
                filename = str(fpath)

            if fileExists:
                res = EricMessageBox.yesNo(
                    self,
                    self.tr("Save Diagram"),
                    self.tr(
                        "<p>The file <b>{0}</b> exists already. Overwrite it?</p>"
                    ).format(filename),
                    icon=EricMessageBox.Warning,
                )
                if not res:
                    return

        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:
            if FileSystemUtilities.isRemoteFileName(self.__project.getProjectPath()):
                filename = EricServerFileDialog.getOpenFileName(
                    self,
                    self.tr("Load Diagram"),
                    self.__project.getProjectPath(),
                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
                )
            else:
                filename = EricFileDialog.getOpenFileName(
                    self,
                    self.tr("Load Diagram"),
                    self.__project.getProjectPath(),
                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
                )
            if not filename:
                # Canceled by user
                return False

        return self.__readJsonGraphicsFile(filename)

    #######################################################################
    ## Methods to read and write eric graphics files of the JSON based
    ## file format.
    #######################################################################

    def __showInvalidDataMessage(self, filename):
        """
        Private slot to show a message dialog indicating an invalid data file.

        @param filename name of the file containing the invalid data
        @type str
        """
        EricMessageBox.critical(
            self,
            self.tr("Load Diagram"),
            self.tr(
                """<p>The file <b>{0}</b> does not contain valid data.</p>"""
            ).format(filename),
        )

    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)
            if FileSystemUtilities.isRemoteFileName(filename):
                self.__remotefsInterface.writeFile(filename, jsonString.encode("utf-8"))
            else:
                with open(filename, "w") as f:
                    f.write(jsonString)
            return True
        except (OSError, TypeError) 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:
            if FileSystemUtilities.isRemoteFileName(filename):
                bdata = self.__remotefsInterface.readFile(filename)
                jsonString = bdata.decode("utf-8")
            else:
                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