eric6/Graphics/UMLDialog.py

Sun, 02 May 2021 17:26:47 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 02 May 2021 17:26:47 +0200
changeset 8282
16b243bdb12f
parent 8281
184ece570a2b
child 8289
871b40c5a77a
permissions
-rw-r--r--

UML Diagrams
- added code to load a saved UML diagram via the file browser or the project others browser

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

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

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

import enum

from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo
from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene

from E5Gui import E5MessageBox, E5FileDialog
from E5Gui.E5MainWindow import E5MainWindow

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(E5MainWindow):
    """
    Class implementing a dialog showing UML like diagrams.
    """
    FileVersions = ("1.0")
    
    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.__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)
        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.__diagramTypeString)
    
    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 (boolean)
        """
        if not fromFile and self.builder:
            self.builder.buildDiagram()
        super().show()
    
    def __relayout(self):
        """
        Private method to relayout 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)
    
    # 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)
        """
        if not filename:
            fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
                self,
                self.tr("Save Diagram"),
                "",
                self.tr("Eric 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.tr("Save Diagram"),
                    self.tr("<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} ({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())
        
        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
    
    # TODO: add loading of file in JSON format
    # TODO: eric7: delete the current one
    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 = E5FileDialog.getOpenFileName(
                self,
                self.tr("Load Diagram"),
                "",
                self.tr("Eric Graphics File (*.e5g);;All Files (*)"))
            if not filename:
                # Cancelled by user
                return False
        
        try:
            with open(filename, "r", encoding="utf-8") as f:
                data = f.read()
        except OSError as err:
            E5MessageBox.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, diagramTypeString = value.strip().split(None, 1)
                self.__diagramType = UMLDialogType(int(diagramType))
                self.__diagramTypeString = diagramTypeString[1:-1]
                # remove opening and closing bracket
            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.__diagramTypeString)
        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 (string)
        @param linenum number of the invalid line (integer)
        """
        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)
        )
        E5MessageBox.critical(self, self.tr("Load Diagram"), msg)

eric ide

mercurial