Wed, 05 May 2021 18:17:24 +0200
UML Diagrams
- added code to save diagrams as JSON files
--- a/docs/changelog Wed May 05 17:29:41 2021 +0200 +++ b/docs/changelog Wed May 05 18:17:24 2021 +0200 @@ -11,6 +11,7 @@ the project others browser -- improved the diagram layout of the Import Diagram and the Application Diagram + -- added code to save diagrams as JSON files Version 21.5: - bug fixes
--- a/eric6/Graphics/ApplicationDiagramBuilder.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/ApplicationDiagramBuilder.py Wed May 05 18:17:24 2021 +0200 @@ -434,3 +434,15 @@ self.initialize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return { + "project_name": self.project.getProjectName(), + "no_modules": self.noModules, + }
--- a/eric6/Graphics/AssociationItem.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/AssociationItem.py Wed May 05 18:17:24 2021 +0200 @@ -602,3 +602,17 @@ topToBottom = Utilities.toBool(value) return src, dst, assocType, topToBottom + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return { + "src": self.itemA.getId(), + "dst": self.itemB.getId(), + "type": self.assocType.value, + "topToBottom": self.topToBottom, + }
--- a/eric6/Graphics/ClassItem.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/ClassItem.py Wed May 05 18:17:24 2021 +0200 @@ -389,3 +389,19 @@ self.__calculateSize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return { + "is_external": self.external, + "no_attributes": self.noAttrs, + "name": self.model.getName(), + "attributes": self.model.getInstanceAttributes(), + "methods": self.model.getMethods(), + "class_attributes": self.model.getClassAttributes(), + }
--- a/eric6/Graphics/ImportsDiagramBuilder.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/ImportsDiagramBuilder.py Wed May 05 18:17:24 2021 +0200 @@ -50,6 +50,12 @@ self.showExternalImports = showExternalImports self.packagePath = os.path.abspath(package) + + self.__relPackagePath = ( + self.project.getRelativePath(self.packagePath) + if self.project.isProjectSource(self.packagePath) else + "" + ) def initialize(self): """ @@ -358,3 +364,23 @@ self.initialize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + data = { + "project_name": self.project.getProjectName(), + "show_external": self.showExternalImports, + } + + data["package"] = ( + Utilities.fromNativeSeparators(self.__relPackagePath) + if self.__relPackagePath else + Utilities.fromNativeSeparators(self.packagePath) + ) + + return data
--- a/eric6/Graphics/ModuleItem.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/ModuleItem.py Wed May 05 18:17:24 2021 +0200 @@ -227,3 +227,15 @@ self.__calculateSize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return { + "name": self.model.getName(), + "classes": self.model.getClasses(), + }
--- a/eric6/Graphics/PackageDiagramBuilder.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/PackageDiagramBuilder.py Wed May 05 18:17:24 2021 +0200 @@ -45,6 +45,12 @@ self.package = os.path.abspath(package) self.noAttrs = noAttrs + + self.__relPackage = ( + self.project.getRelativePath(self.package) + if self.project.isProjectSource(self.package) else + "" + ) def initialize(self): """ @@ -502,3 +508,23 @@ self.initialize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + data = { + "project_name": self.project.getProjectName(), + "no_attributes": self.noAttrs, + } + + data["package"] = ( + Utilities.fromNativeSeparators(self.__relPackage) + if self.__relPackage else + Utilities.fromNativeSeparators(self.package) + ) + + return data
--- a/eric6/Graphics/PackageItem.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/PackageItem.py Wed May 05 18:17:24 2021 +0200 @@ -256,3 +256,16 @@ self.__calculateSize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return { + "name": self.model.getName(), + "no_nodules": self.noModules, + "modules": self.model.getModules(), + }
--- a/eric6/Graphics/UMLClassDiagramBuilder.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/UMLClassDiagramBuilder.py Wed May 05 18:17:24 2021 +0200 @@ -41,6 +41,12 @@ self.file = file self.noAttrs = noAttrs + + self.__relFile = ( + self.project.getRelativePath(self.file) + if self.project.isProjectSource(self.file) else + "" + ) def initialize(self): """ @@ -339,3 +345,23 @@ self.initialize() return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + data = { + "project_name": self.project.getProjectName(), + "no_attributes": self.noAttrs, + } + + data["file"] = ( + Utilities.fromNativeSeparators(self.__relFile) + if self.__relFile else + Utilities.fromNativeSeparators(self.file) + ) + + return data
--- a/eric6/Graphics/UMLDiagramBuilder.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/UMLDiagramBuilder.py Wed May 05 18:17:24 2021 +0200 @@ -70,3 +70,12 @@ @rtype bool """ return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return {}
--- a/eric6/Graphics/UMLDialog.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/UMLDialog.py Wed May 05 18:17:24 2021 +0200 @@ -8,8 +8,9 @@ """ import enum +import json -from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo +from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo, QCoreApplication from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene from E5Gui import E5MessageBox, E5FileDialog @@ -36,6 +37,17 @@ """ 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): """ @@ -60,12 +72,6 @@ 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) @@ -84,7 +90,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): """ @@ -208,7 +227,6 @@ """ self.__saveAs(self.__fileName) - # TODO: change this to save file in JSON format @pyqtSlot() def __saveAs(self, filename=""): """ @@ -222,7 +240,9 @@ 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: @@ -243,31 +263,16 @@ 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()) + 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: eric7: delete the current one @@ -290,6 +295,26 @@ # Canceled by user return False + if filename.endswith(".e5g"): + return self.__readLineBasedGraphicsFile(filename) + else: + return False + + ####################################################################### + ## 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() @@ -327,10 +352,8 @@ self.__showInvalidDataMessage(filename, linenum) return False try: - diagramType, diagramTypeString = value.strip().split(None, 1) + diagramType = value.strip().split(None, 1)[0] self.__diagramType = UMLDialogType(int(diagramType)) - self.__diagramTypeString = diagramTypeString[1:-1] - # remove opening and closing bracket except ValueError: self.__showInvalidDataMessage(filename, linenum) return False @@ -373,11 +396,47 @@ # 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. @@ -396,3 +455,42 @@ ).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
--- a/eric6/Graphics/UMLGraphicsView.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/UMLGraphicsView.py Wed May 05 18:17:24 2021 +0200 @@ -806,3 +806,35 @@ self.scene().addItem(assoc) return True, -1 + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + items = [] + for item in self.filteredItems(self.scene().items(), UMLItem): + items.append({ + "id": item.getId(), + "x": item.x(), + "y": item.y(), + "type": item.getItemType(), + "data": item.toDict() + }) + + from .AssociationItem import AssociationItem + associations = [ + assoc.toDict() + for assoc in self.filteredItems(self.scene().items(), + AssociationItem) + ] + + data = { + "diagram_name": self.diagramName, + "items": items, + "associations": associations, + } + + return data
--- a/eric6/Graphics/UMLItem.py Wed May 05 17:29:41 2021 +0200 +++ b/eric6/Graphics/UMLItem.py Wed May 05 18:17:24 2021 +0200 @@ -276,3 +276,12 @@ @rtype bool """ return True + + def toDict(self): + """ + Public method to collect data to be persisted. + + @return dictionary containing data to be persisted + @rtype dict + """ + return {}