eric6/Graphics/ImportsDiagramBuilder.py

branch
maintenance
changeset 8400
b3eefd7e58d1
parent 8273
698ae46f40a4
parent 8295
3f5e8b0a338e
--- a/eric6/Graphics/ImportsDiagramBuilder.py	Sat May 01 14:27:38 2021 +0200
+++ b/eric6/Graphics/ImportsDiagramBuilder.py	Thu Jun 03 11:39:23 2021 +0200
@@ -32,19 +32,30 @@
         """
         Constructor
         
-        @param dialog reference to the UML dialog (UMLDialog)
-        @param view reference to the view object (UMLGraphicsView)
-        @param project reference to the project object (Project)
+        @param dialog reference to the UML dialog
+        @type UMLDialog
+        @param view reference to the view object
+        @type UMLGraphicsView
+        @param project reference to the project object
+        @type Project
         @param package name of a python package to show the import
-            relationships (string)
+            relationships
+        @type str
         @param showExternalImports flag indicating to show exports from
-            outside the package (boolean)
+            outside the package
+        @type bool
         """
         super().__init__(dialog, view, project)
         self.setObjectName("ImportsDiagram")
         
         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):
         """
@@ -74,7 +85,8 @@
         Private method to build a dictionary of modules contained in the
         package.
         
-        @return dictionary of modules contained in the package.
+        @return dictionary of modules contained in the package
+        @rtype dict
         """
         import Utilities.ModuleParser
         extensions = (
@@ -121,115 +133,97 @@
         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
         
-        shapes = {}
-        p = 10
-        y = 10
-        maxHeight = 0
-        sceneRect = self.umlView.sceneRect()
+        self.__shapes = {}
         
         modules = self.__buildModulesDict()
-        sortedkeys = sorted(modules.keys())
         externalMods = []
         packageList = self.shortPackage.split('.')
         packageListLen = len(packageList)
-        for module in sortedkeys:
+        for module in sorted(modules.keys()):
             impLst = []
-            for i in modules[module].imports:
-                n = (i[len(self.package) + 1:]
-                     if i.startswith(self.package) else i)
-                if i in modules:
+            for importName in modules[module].imports:
+                n = (
+                    importName[len(self.package) + 1:]
+                    if importName.startswith(self.package) else
+                    importName
+                )
+                if importName in modules:
                     impLst.append(n)
                 elif self.showExternalImports:
                     impLst.append(n)
                     if n not in externalMods:
                         externalMods.append(n)
-            for i in list(modules[module].from_imports.keys()):
-                if i.startswith('.'):
-                    dots = len(i) - len(i.lstrip('.'))
+            for importName in list(modules[module].from_imports.keys()):
+                if importName.startswith('.'):
+                    dots = len(importName) - len(importName.lstrip('.'))
                     if dots == 1:
-                        n = i[1:]
-                        i = n
+                        n = importName[1:]
+                        importName = n
                     else:
                         if self.showExternalImports:
                             n = '.'.join(
                                 packageList[:packageListLen - dots + 1] +
-                                [i[dots:]])
+                                [importName[dots:]])
                         else:
-                            n = i
-                elif i.startswith(self.package):
-                    n = i[len(self.package) + 1:]
+                            n = importName
+                elif importName.startswith(self.package):
+                    n = importName[len(self.package) + 1:]
                 else:
-                    n = i
-                if i in modules:
+                    n = importName
+                if importName in modules:
                     impLst.append(n)
                 elif self.showExternalImports:
                     impLst.append(n)
                     if n not in externalMods:
                         externalMods.append(n)
+            
             classNames = []
-            for cls in list(modules[module].classes.keys()):
-                className = modules[module].classes[cls].name
+            for class_ in list(modules[module].classes.keys()):
+                className = modules[module].classes[class_].name
                 if className not in classNames:
                     classNames.append(className)
             shape = self.__addModule(module, classNames, 0.0, 0.0)
-            shapeRect = shape.sceneBoundingRect()
-            shapes[module] = (shape, impLst)
-            pn = p + shapeRect.width() + 10
-            maxHeight = max(maxHeight, shapeRect.height())
-            if pn > sceneRect.width():
-                p = 10
-                y += maxHeight + 10
-                maxHeight = shapeRect.height()
-                shape.setPos(p, y)
-                p += shapeRect.width() + 10
-            else:
-                shape.setPos(p, y)
-                p = pn
+            self.__shapes[module] = (shape, impLst)
         
         for module in externalMods:
             shape = self.__addModule(module, [], 0.0, 0.0)
-            shapeRect = shape.sceneBoundingRect()
-            shapes[module] = (shape, [])
-            pn = p + shapeRect.width() + 10
-            maxHeight = max(maxHeight, shapeRect.height())
-            if pn > sceneRect.width():
-                p = 10
-                y += maxHeight + 10
-                maxHeight = shapeRect.height()
-                shape.setPos(p, y)
-                p += shapeRect.width() + 10
-            else:
-                shape.setPos(p, y)
-                p = pn
+            self.__shapes[module] = (shape, [])
         
-        rect = self.umlView._getDiagramRect(10)
-        sceneRect = self.umlView.sceneRect()
-        if rect.width() > sceneRect.width():
-            sceneRect.setWidth(rect.width())
-        if rect.height() > sceneRect.height():
-            sceneRect.setHeight(rect.height())
-        self.umlView.setSceneSize(sceneRect.width(), sceneRect.height())
+        # build a list of routes
+        nodes = []
+        routes = []
+        for module in self.__shapes:
+            nodes.append(module)
+            for rel in self.__shapes[module][1]:
+                route = (module, rel)
+                if route not in routes:
+                    routes.append(route)
         
-        self.__createAssociations(shapes)
+        self.__arrangeNodes(nodes, routes[:])
+        self.__createAssociations(routes)
         self.umlView.autoAdjustSceneSize(limit=True)
     
     def __addModule(self, name, classes, x, y):
         """
         Private method to add a module to the diagram.
         
-        @param name module name to be shown (string)
+        @param name module name to be shown
+        @type str
         @param classes list of class names contained in the module
-            (list of strings)
-        @param x x-coordinate (float)
-        @param y y-coordinate (float)
-        @return reference to the imports item (ModuleItem)
+        @type list of str
+        @param x x-coordinate
+        @type float
+        @param y y-coordinate
+        @type float
+        @return reference to the imports item
+        @rtype ModuleItem
         """
         from .ModuleItem import ModuleItem, ModuleModel
         classes.sort()
@@ -239,26 +233,107 @@
         impW.setId(self.umlView.getItemId())
         return impW
     
-    def __createAssociations(self, shapes):
+    def __arrangeNodes(self, nodes, routes, whiteSpaceFactor=1.2):
+        """
+        Private method to arrange the shapes on the canvas.
+        
+        The algorithm is borrowed from Boa Constructor.
+        
+        @param nodes list of nodes to arrange
+        @type list of str
+        @param routes list of routes
+        @type list of tuple of (str, str)
+        @param whiteSpaceFactor factor to increase whitespace between
+            items
+        @type float
+        """
+        from . import GraphicsUtilities
+        generations = GraphicsUtilities.sort(nodes, routes)
+        
+        # calculate width and height of all elements
+        sizes = []
+        for generation in generations:
+            sizes.append([])
+            for child in generation:
+                sizes[-1].append(
+                    self.__shapes[child][0].sceneBoundingRect())
+                
+        # calculate total width and total height
+        width = 0
+        height = 0
+        widths = []
+        heights = []
+        for generation in sizes:
+            currentWidth = 0
+            currentHeight = 0
+            
+            for rect in generation:
+                if rect.height() > currentHeight:
+                    currentHeight = rect.height()
+                currentWidth += rect.width()
+                
+            # update totals
+            if currentWidth > width:
+                width = currentWidth
+            height += currentHeight
+            
+            # store generation info
+            widths.append(currentWidth)
+            heights.append(currentHeight)
+        
+        # add in some whitespace
+        width *= whiteSpaceFactor
+        height = height * whiteSpaceFactor - 20
+        verticalWhiteSpace = 40.0
+        
+        sceneRect = self.umlView.sceneRect()
+        width += 50.0
+        height += 50.0
+        swidth = sceneRect.width() if width < sceneRect.width() else width
+        sheight = sceneRect.height() if height < sceneRect.height() else height
+        self.umlView.setSceneSize(swidth, sheight)
+        
+        # distribute each generation across the width and the
+        # generations across height
+        y = 10.0
+        for currentWidth, currentHeight, generation in (
+            zip(reversed(widths), reversed(heights), reversed(generations))
+        ):
+            x = 10.0
+            # whiteSpace is the space between any two elements
+            whiteSpace = (
+                (width - currentWidth - 20) /
+                (len(generation) - 1.0 or 2.0)
+            )
+            for name in generation:
+                shape = self.__shapes[name][0]
+                shape.setPos(x, y)
+                rect = shape.sceneBoundingRect()
+                x = x + rect.width() + whiteSpace
+            y = y + currentHeight + verticalWhiteSpace
+    
+    def __createAssociations(self, routes):
         """
         Private method to generate the associations between the module shapes.
         
-        @param shapes list of shapes
+        @param routes list of associations
+        @type list of tuple of (str, str)
         """
         from .AssociationItem import AssociationItem, AssociationType
-        for module in list(shapes.keys()):
-            for rel in shapes[module][1]:
-                assoc = AssociationItem(
-                    shapes[module][0], shapes[rel][0],
-                    AssociationType.IMPORTS,
-                    colors=self.umlView.getDrawingColors())
-                self.scene.addItem(assoc)
+        for route in routes:
+            assoc = AssociationItem(
+                self.__shapes[route[0]][0],
+                self.__shapes[route[1]][0],
+                AssociationType.IMPORTS,
+                colors=self.umlView.getDrawingColors())
+            self.scene.addItem(assoc)
     
     def getPersistenceData(self):
         """
         Public method to get a string for data to be persisted.
         
-        @return persisted data string (string)
+        @return persisted data string
+        @rtype str
         """
         return "package={0}, show_external={1}".format(
             self.packagePath, self.showExternalImports)
@@ -267,9 +342,12 @@
         """
         Public method to parse persisted data.
         
-        @param version version of the data (string)
-        @param data persisted data to be parsed (string)
-        @return flag indicating success (boolean)
+        @param version version of the data
+        @type str
+        @param data persisted data to be parsed
+        @type str
+        @return flag indicating success
+        @rtype bool
         """
         parts = data.split(", ")
         if (
@@ -286,3 +364,60 @@
         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
+    
+    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, ""

eric ide

mercurial