Sat, 08 May 2021 18:34:08 +0200
UML Diagrams
- added code to load diagrams saved as JSON files
- changed the prining code
--- a/docs/changelog Thu May 06 19:46:00 2021 +0200 +++ b/docs/changelog Sat May 08 18:34:08 2021 +0200 @@ -11,7 +11,8 @@ the project others browser -- improved the diagram layout of the Import Diagram and the Application Diagram - -- added code to save diagrams as JSON files + -- added code to save and load diagrams as JSON files + -- changed print code Version 21.5: - bug fixes
--- a/eric6/E5Graphics/E5GraphicsView.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/E5Graphics/E5GraphicsView.py Sat May 08 18:34:08 2021 +0200 @@ -345,22 +345,24 @@ (QPrinter) @param diagramName name of the diagram (string) """ - painter = QPainter() - painter.begin(printer) - offsetX = 0 - offsetY = 0 - widthX = 0 - heightY = 0 + painter = QPainter(printer) + font = QFont("times", 10) painter.setFont(font) fm = painter.fontMetrics() fontHeight = fm.lineSpacing() - marginX = printer.pageRect().x() - printer.paperRect().x() + marginX = ( + printer.pageLayout().paintRectPixels(printer.resolution()).x() - + printer.pageLayout().fullRectPixels(printer.resolution()).x() + ) marginX = ( Preferences.getPrinter("LeftMargin") * int(printer.resolution() / 2.54) - marginX ) - marginY = printer.pageRect().y() - printer.paperRect().y() + marginY = ( + printer.pageLayout().paintRectPixels(printer.resolution()).y() - + printer.pageLayout().fullRectPixels(printer.resolution()).y() + ) marginY = ( Preferences.getPrinter("TopMargin") * int(printer.resolution() / 2.54) - marginY @@ -377,59 +379,18 @@ int(printer.resolution() / 2.54) ) - border = self.border == 0 and 5 or self.border - rect = self._getDiagramRect(border) - diagram = self.__getDiagram(rect) + self.scene().render(painter, + target=QRectF(marginX, marginY, width, height)) - finishX = False - finishY = False - page = 0 - pageX = 0 - pageY = 1 - while not finishX or not finishY: - if not finishX: - offsetX = pageX * width - pageX += 1 - elif not finishY: - offsetY = pageY * height - offsetX = 0 - pageY += 1 - finishX = False - pageX = 1 - if (width + offsetX) > diagram.width(): - finishX = True - widthX = diagram.width() - offsetX - else: - widthX = width - if diagram.width() < width: - widthX = diagram.width() - finishX = True - offsetX = 0 - if (height + offsetY) > diagram.height(): - finishY = True - heightY = diagram.height() - offsetY - else: - heightY = height - if diagram.height() < height: - finishY = True - heightY = diagram.height() - offsetY = 0 - - painter.drawPixmap(marginX, marginY, diagram, - offsetX, offsetY, widthX, heightY) - # write a foot note - s = self.tr("{0}, Page {1}").format(diagramName, page + 1) - tc = QColor(50, 50, 50) - painter.setPen(tc) - painter.drawRect(marginX, marginY, width, height) - painter.drawLine(marginX, marginY + height + 2, - marginX + width, marginY + height + 2) - painter.setFont(font) - painter.drawText(marginX, marginY + height + 4, width, - fontHeight, Qt.AlignmentFlag.AlignRight, s) - if not finishX or not finishY: - printer.newPage() - page += 1 + # write a foot note + tc = QColor(50, 50, 50) + painter.setPen(tc) + painter.drawRect(marginX, marginY, width, height) + painter.drawLine(marginX, marginY + height + 2, + marginX + width, marginY + height + 2) + painter.setFont(font) + painter.drawText(marginX, marginY + height + 4, width, + fontHeight, Qt.AlignmentFlag.AlignRight, diagramName) painter.end()
--- a/eric6/Graphics/ApplicationDiagramBuilder.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/ApplicationDiagramBuilder.py Sat May 08 18:34:08 2021 +0200 @@ -446,3 +446,31 @@ "project_name": self.project.getProjectName(), "no_modules": self.noModules, } + + def fromDict(self, version, data): + """ + Public method to populate the class with data persisted by 'toDict()'. + + @param version version of the data + @type str + @param data dictionary containing the persisted data + @type dict + @return tuple containing a flag indicating success and an info + message in case the diagram belongs to a different project + @rtype tuple of (bool, str) + """ + try: + self.noModules = data["no_modules"] + + if data["project_name"] != self.project.getProjectName(): + msg = self.tr( + "<p>The diagram belongs to project <b>{0}</b>." + " Please open it and try again.</p>" + ).format(data["project_name"]) + return False, msg + except KeyError: + return False, "" + + self.initialize() + + return True, ""
--- a/eric6/Graphics/AssociationItem.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/AssociationItem.py Sat May 08 18:34:08 2021 +0200 @@ -616,3 +616,27 @@ "type": self.assocType.value, "topToBottom": self.topToBottom, } + + @classmethod + def fromDict(cls, data, umlItems, colors=None): + """ + Class method to create an association item from persisted data. + + @param data dictionary containing the persisted data as generated + by toDict() + @type dict + @param umlItems list of UML items + @type list of UMLItem + @param colors tuple containing the foreground and background colors + @type tuple of (QColor, QColor) + @return created association item + @rtype AssociationItem + """ + try: + return cls(umlItems[data["src"]], + umlItems[data["dst"]], + assocType=AssociationType(data["type"]), + topToBottom=data["topToBottom"], + colors=colors) + except (KeyError, ValueError): + return None
--- a/eric6/Graphics/ClassItem.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/ClassItem.py Sat May 08 18:34:08 2021 +0200 @@ -139,7 +139,8 @@ self.external = external self.noAttrs = noAttrs - scene.addItem(self) + if scene: + scene.addItem(self) if self.model: self.__createTexts() @@ -398,10 +399,44 @@ @rtype dict """ return { + "id": self.getId(), + "x": self.x(), + "y": self.y(), + "type": self.getItemType(), "is_external": self.external, "no_attributes": self.noAttrs, - "name": self.model.getName(), + "model_name": self.model.getName(), "attributes": self.model.getInstanceAttributes(), "methods": self.model.getMethods(), "class_attributes": self.model.getClassAttributes(), } + + @classmethod + def fromDict(cls, data, colors=None): + """ + Class method to create a class item from persisted data. + + @param data dictionary containing the persisted data as generated + by toDict() + @type dict + @param colors tuple containing the foreground and background colors + @type tuple of (QColor, QColor) + @return created class item + @rtype ClassItem + """ + try: + model = ClassModel(data["model_name"], + data["methods"], + data["attributes"], + data["class_attributes"]) + itm = cls(model=model, + external=data["is_external"], + x=0, + y=0, + noAttrs=data["no_attributes"], + colors=colors) + itm.setPos(data["x"], data["y"]) + itm.setId(data["id"]) + return itm + except KeyError: + return None
--- a/eric6/Graphics/ImportsDiagramBuilder.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/ImportsDiagramBuilder.py Sat May 08 18:34:08 2021 +0200 @@ -133,10 +133,10 @@ initlist = glob.glob(os.path.join(self.packagePath, '__init__.*')) if len(initlist) == 0: ct = QGraphicsTextItem(None) - ct.setHtml( - self.tr( - "The directory <b>'{0}'</b> is not a Python package.") - .format(self.package)) + ct.setHtml(self.buildErrorMessage( + self.tr("The directory <b>'{0}'</b> is not a Python" + " package.").format(self.package) + )) self.scene.addItem(ct) return @@ -384,3 +384,40 @@ ) return data + + def fromDict(self, version, data): + """ + Public method to populate the class with data persisted by 'toDict()'. + + @param version version of the data + @type str + @param data dictionary containing the persisted data + @type dict + @return tuple containing a flag indicating success and an info + message in case the diagram belongs to a different project + @rtype tuple of (bool, str) + """ + try: + self.showExternalImports = data["show_external"] + + packagePath = Utilities.toNativeSeparators(data["package"]) + if os.path.isabs(packagePath): + self.packagePath = packagePath + self.__relPackagePath = "" + else: + # relative package paths indicate a project package + if data["project_name"] != self.project.getProjectName(): + msg = self.tr( + "<p>The diagram belongs to project <b>{0}</b>." + " Please open it and try again.</p>" + ).format(data["project_name"]) + return False, msg + + self.__relPackagePath = packagePath + self.package = self.project.getAbsolutePath(packagePath) + except KeyError: + return False, "" + + self.initialize() + + return True, ""
--- a/eric6/Graphics/ModuleItem.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/ModuleItem.py Sat May 08 18:34:08 2021 +0200 @@ -77,7 +77,8 @@ """ UMLItem.__init__(self, model, x, y, rounded, colors, parent) - scene.addItem(self) + if scene: + scene.addItem(self) if self.model: self.__createTexts() @@ -236,6 +237,36 @@ @rtype dict """ return { - "name": self.model.getName(), + "id": self.getId(), + "x": self.x(), + "y": self.y(), + "type": self.getItemType(), + "model_name": self.model.getName(), "classes": self.model.getClasses(), } + + @classmethod + def fromDict(cls, data, colors=None): + """ + Class method to create a class item from persisted data. + + @param data dictionary containing the persisted data as generated + by toDict() + @type dict + @param colors tuple containing the foreground and background colors + @type tuple of (QColor, QColor) + @return created class item + @rtype ClassItem + """ + try: + model = ModuleModel(data["model_name"], + data["classes"]) + itm = cls(model, + x=0, + y=0, + colors=colors) + itm.setPos(data["x"], data["y"]) + itm.setId(data["id"]) + return itm + except KeyError: + return None
--- a/eric6/Graphics/PackageDiagramBuilder.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/PackageDiagramBuilder.py Sat May 08 18:34:08 2021 +0200 @@ -222,13 +222,15 @@ return modules = self.__buildModulesDict() - if not modules: + subpackages = self.__buildSubpackagesDict() + + if not modules and not subpackages: ct = QGraphicsTextItem(None) self.scene.addItem(ct) - ct.setHtml( - self.tr( - "The package <b>'{0}'</b> does not contain any modules.") - .format(self.package)) + ct.setHtml(self.buildErrorMessage( + self.tr("The package <b>'{0}'</b> does not contain any modules" + " or subpackages.").format(self.package) + )) return # step 1: build all classes found in the modules @@ -239,13 +241,13 @@ for cls in list(module.classes.keys()): classesFound = True self.__addLocalClass(cls, module.classes[cls], 0, 0) - if not classesFound: + if not classesFound and not subpackages: ct = QGraphicsTextItem(None) self.scene.addItem(ct) - ct.setHtml( - self.tr( - "The package <b>'{0}'</b> does not contain any classes.") - .format(self.package)) + ct.setHtml(self.buildErrorMessage( + self.tr("The package <b>'{0}'</b> does not contain any" + " classes or subpackages.").format(self.package) + )) return # step 2: build the class hierarchies @@ -296,7 +298,6 @@ del todo[0] # step 3: build the subpackages - subpackages = self.__buildSubpackagesDict() for subpackage in sorted(subpackages.keys()): self.__addPackage(subpackage, subpackages[subpackage], 0, 0) nodes.append(subpackage) @@ -528,3 +529,40 @@ ) return data + + def fromDict(self, version, data): + """ + Public method to populate the class with data persisted by 'toDict()'. + + @param version version of the data + @type str + @param data dictionary containing the persisted data + @type dict + @return tuple containing a flag indicating success and an info + message in case the diagram belongs to a different project + @rtype tuple of (bool, str) + """ + try: + self.noAttrs = data["no_attributes"] + + package = Utilities.toNativeSeparators(data["package"]) + if os.path.isabs(package): + self.package = package + self.__relPackage = "" + else: + # relative package paths indicate a project package + if data["project_name"] != self.project.getProjectName(): + msg = self.tr( + "<p>The diagram belongs to project <b>{0}</b>." + " Please open it and try again.</p>" + ).format(data["project_name"]) + return False, msg + + self.__relPackage = package + self.package = self.project.getAbsolutePath(package) + except KeyError: + return False, "" + + self.initialize() + + return True, ""
--- a/eric6/Graphics/PackageItem.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/PackageItem.py Sat May 08 18:34:08 2021 +0200 @@ -83,7 +83,8 @@ UMLItem.__init__(self, model, x, y, rounded, colors, parent) self.noModules = noModules - scene.addItem(self) + if scene: + scene.addItem(self) if self.model: self.__createTexts() @@ -265,7 +266,38 @@ @rtype dict """ return { - "name": self.model.getName(), + "id": self.getId(), + "x": self.x(), + "y": self.y(), + "type": self.getItemType(), + "model_name": self.model.getName(), "no_nodules": self.noModules, "modules": self.model.getModules(), } + + @classmethod + def fromDict(cls, data, colors=None): + """ + Class method to create a class item from persisted data. + + @param data dictionary containing the persisted data as generated + by toDict() + @type dict + @param colors tuple containing the foreground and background colors + @type tuple of (QColor, QColor) + @return created class item + @rtype ClassItem + """ + try: + model = PackageModel(data["model_name"], + data["modules"]) + itm = cls(model, + x=0, + y=0, + noModules=data["no_nodules"], + colors=colors) + itm.setPos(data["x"], data["y"]) + itm.setId(data["id"]) + return itm + except KeyError: + return None
--- a/eric6/Graphics/UMLClassDiagramBuilder.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/UMLClassDiagramBuilder.py Sat May 08 18:34:08 2021 +0200 @@ -8,6 +8,7 @@ """ from itertools import zip_longest +import os from PyQt5.QtWidgets import QGraphicsTextItem @@ -92,9 +93,10 @@ self.file, extensions=extensions, caching=False) except ImportError: ct = QGraphicsTextItem(None) - ct.setHtml( + ct.setHtml(self.buildErrorMessage( self.tr("The module <b>'{0}'</b> could not be found.") - .format(self.file)) + .format(self.file) + )) self.scene.addItem(ct) return @@ -154,9 +156,10 @@ self.umlView.autoAdjustSceneSize(limit=True) else: ct = QGraphicsTextItem(None) - ct.setHtml(self.tr( - "The module <b>'{0}'</b> does not contain any classes.") - .format(self.file)) + ct.setHtml(self.buildErrorMessage( + self.tr("The module <b>'{0}'</b> does not contain any" + " classes.").format(self.file) + )) self.scene.addItem(ct) def __arrangeClasses(self, nodes, routes, whiteSpaceFactor=1.2): @@ -365,3 +368,40 @@ ) return data + + def fromDict(self, version, data): + """ + Public method to populate the class with data persisted by 'toDict()'. + + @param version version of the data + @type str + @param data dictionary containing the persisted data + @type dict + @return tuple containing a flag indicating success and an info + message in case the diagram belongs to a different project + @rtype tuple of (bool, str) + """ + try: + self.noAttrs = data["no_attributes"] + + file = Utilities.toNativeSeparators(data["file"]) + if os.path.isabs(file): + self.file = file + self.__relFile = "" + else: + # relative file paths indicate a project file + if data["project_name"] != self.project.getProjectName(): + msg = self.tr( + "<p>The diagram belongs to project <b>{0}</b>." + " Please open it and try again.</p>" + ).format(data["project_name"]) + return False, msg + + self.__relFile = file + self.file = self.project.getAbsolutePath(file) + except KeyError: + return False, "" + + self.initialize() + + return True, ""
--- a/eric6/Graphics/UMLDiagramBuilder.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/UMLDiagramBuilder.py Sat May 08 18:34:08 2021 +0200 @@ -37,6 +37,22 @@ """ return + def buildErrorMessage(self, msg): + """ + Public method to build an error string to be included in the scene. + + @param msg error message + @type str + @return prepared error string + @rtype str + """ + return ( + "<font color='{0}'>".format( + self.umlView.getDrawingColors()[0].name()) + + msg + + "</font>" + ) + def buildDiagram(self): """ Public method to build the diagram. @@ -79,3 +95,17 @@ @rtype dict """ return {} + + def fromDict(self, version, data): + """ + Public method to populate the class with data persisted by 'toDict()'. + + @param version version of the data + @type str + @param data dictionary containing the persisted data + @type dict + @return tuple containing a flag indicating success and an info + message in case the diagram belongs to a different project + @rtype tuple of (bool, str) + """ + return True, ""
--- a/eric6/Graphics/UMLDialog.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/UMLDialog.py Sat May 08 18:34:08 2021 +0200 @@ -35,7 +35,8 @@ """ Class implementing a dialog showing UML like diagrams. """ - FileVersions = ("1.0") + FileVersions = ("1.0", ) + JsonFileVersions = ("1.0", ) UMLDialogType2String = { UMLDialogType.CLASS_DIAGRAM: @@ -274,7 +275,6 @@ # 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 def load(self, filename=""): """ @@ -290,15 +290,19 @@ self, self.tr("Load Diagram"), "", - self.tr("Eric Graphics File (*.e5g);;All Files (*)")) + self.tr("Eric Graphics File (*.egj);;" + "Eric Text Graphics File (*.e5g);;" + "All Files (*)")) if not filename: # Canceled by user return False - if filename.endswith(".e5g"): - return self.__readLineBasedGraphicsFile(filename) - else: - 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 @@ -492,5 +496,86 @@ 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))) + """<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
--- a/eric6/Graphics/UMLGraphicsView.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/UMLGraphicsView.py Sat May 08 18:34:08 2021 +0200 @@ -171,7 +171,17 @@ self.alignMapper.setMapping( self.alignBottomAct, Qt.AlignmentFlag.AlignBottom) self.alignBottomAct.triggered.connect(self.alignMapper.map) + + def setLayoutActionsEnabled(self, enable): + """ + Public method to enable or disable the layout related actions. + @param enable flag indicating the desired enable state + @type bool + """ + self.rescanAct.setEnabled(enable) + self.relayoutAct.setEnabled(enable) + def __checkSizeActions(self): """ Private slot to set the enabled state of the size actions. @@ -420,7 +430,7 @@ """ Public slot called to print the diagram. """ - printer = QPrinter(mode=QPrinter.PrinterMode.ScreenResolution) + printer = QPrinter(mode=QPrinter.PrinterMode.PrinterResolution) printer.setFullPage(True) if Preferences.getPrinter("ColorMode"): printer.setColorMode(QPrinter.ColorMode.Color) @@ -452,7 +462,7 @@ """ from PyQt5.QtPrintSupport import QPrintPreviewDialog - printer = QPrinter(mode=QPrinter.PrinterMode.ScreenResolution) + printer = QPrinter(mode=QPrinter.PrinterMode.PrinterResolution) printer.setFullPage(True) if Preferences.getPrinter("ColorMode"): printer.setColorMode(QPrinter.ColorMode.Color) @@ -782,14 +792,15 @@ y = float(y.split("=", 1)[1].strip()) itemType = itemType.split("=", 1)[1].strip() if itemType == ClassItem.ItemType: - itm = ClassItem(x=x, y=y, scene=self.scene(), + itm = ClassItem(x=0, y=0, scene=self.scene(), colors=self.getDrawingColors()) elif itemType == ModuleItem.ItemType: - itm = ModuleItem(x=x, y=y, scene=self.scene(), + itm = ModuleItem(x=0, y=0, scene=self.scene(), colors=self.getDrawingColors()) elif itemType == PackageItem.ItemType: - itm = PackageItem(x=x, y=y, scene=self.scene(), + itm = PackageItem(x=0, y=0, scene=self.scene(), colors=self.getDrawingColors()) + itm.setPos(x, y) itm.setId(itemId) umlItems[itemId] = itm if not itm.parseItemDataString(version, itemData): @@ -814,15 +825,10 @@ @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() - }) + items = [ + item.toDict() + for item in self.filteredItems(self.scene().items(), UMLItem) + ] from .AssociationItem import AssociationItem associations = [ @@ -838,3 +844,50 @@ } return data + + def fromDict(self, version, data): + """ + Public method to populate the class with data persisted by 'toDict()'. + + @param version version of the data + @type str + @param data dictionary containing the persisted data + @type dict + @return flag indicating success + @rtype bool + """ + from .UMLItem import UMLItem + from .ClassItem import ClassItem + from .ModuleItem import ModuleItem + from .PackageItem import PackageItem + from .AssociationItem import AssociationItem + + umlItems = {} + + try: + self.diagramName = data["diagram_name"] + for itemData in data["items"]: + if itemData["type"] == UMLItem.ItemType: + itm = UMLItem.fromDict( + itemData, colors=self.getDrawingColors()) + elif itemData["type"] == ClassItem.ItemType: + itm = ClassItem.fromDict( + itemData, colors=self.getDrawingColors()) + elif itemData["type"] == ModuleItem.ItemType: + itm = ModuleItem.fromDict( + itemData, colors=self.getDrawingColors()) + elif itemData["type"] == PackageItem.ItemType: + itm = PackageItem.fromDict( + itemData, colors=self.getDrawingColors()) + if itm is not None: + umlItems[itm.getId()] = itm + self.scene().addItem(itm) + + for assocData in data["associations"]: + assoc = AssociationItem.fromDict( + assocData, umlItems, colors=self.getDrawingColors()) + self.scene().addItem(assoc) + + return True + except KeyError: + return False
--- a/eric6/Graphics/UMLItem.py Thu May 06 19:46:00 2021 +0200 +++ b/eric6/Graphics/UMLItem.py Sat May 08 18:34:08 2021 +0200 @@ -190,12 +190,14 @@ self.shouldAdjustAssociations = True # 2. ensure the new position is inside the scene - rect = self.scene().sceneRect() - if not rect.contains(value): - # keep the item inside the scene - value.setX(min(rect.right(), max(value.x(), rect.left()))) - value.setY(min(rect.bottom(), max(value.y(), rect.top()))) - return value + scene = self.scene() + if scene: + rect = scene.sceneRect() + if not rect.contains(value): + # keep the item inside the scene + value.setX(min(rect.right(), max(value.x(), rect.left()))) + value.setY(min(rect.bottom(), max(value.y(), rect.top()))) + return value return QGraphicsItem.itemChange(self, change, value) @@ -284,4 +286,35 @@ @return dictionary containing data to be persisted @rtype dict """ - return {} + return { + "id": self.getId(), + "x": self.x(), + "y": self.y(), + "type": self.getItemType(), + "model_name": self.model.getName(), + } + + @classmethod + def fromDict(cls, data, colors=None): + """ + Class method to create a generic UML item from persisted data. + + @param data dictionary containing the persisted data as generated + by toDict() + @type dict + @param colors tuple containing the foreground and background colors + @type tuple of (QColor, QColor) + @return created UML item + @rtype UMLItem + """ + try: + model = UMLModel(data["model_name"]) + itm = cls(model=model, + x=0, + y=0, + colors=colors) + itm.setPos(data["x"], data["y"]) + itm.setId(data["id"]) + return itm + except KeyError: + return None