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