UML Diagrams

Sat, 08 May 2021 18:34:08 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 08 May 2021 18:34:08 +0200
changeset 8295
3f5e8b0a338e
parent 8294
cb4e5bbf3a2c
child 8296
14f33eededf7

UML Diagrams
- added code to load diagrams saved as JSON files
- changed the prining code

docs/changelog file | annotate | diff | comparison | revisions
eric6/E5Graphics/E5GraphicsView.py file | annotate | diff | comparison | revisions
eric6/Graphics/ApplicationDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/AssociationItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/ClassItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/ImportsDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/ModuleItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/PackageDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/PackageItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLClassDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLDialog.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLGraphicsView.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLItem.py file | annotate | diff | comparison | revisions
--- 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

eric ide

mercurial