eric6/Graphics/ApplicationDiagramBuilder.py

changeset 8286
62ae22eae123
parent 8270
6ba3564b7161
child 8287
30eb7bc13d63
--- a/eric6/Graphics/ApplicationDiagramBuilder.py	Sun May 02 18:16:54 2021 +0200
+++ b/eric6/Graphics/ApplicationDiagramBuilder.py	Mon May 03 19:42:27 2021 +0200
@@ -10,7 +10,7 @@
 import os
 import glob
 
-from PyQt5.QtWidgets import QApplication
+from PyQt5.QtWidgets import QApplication, QInputDialog
 
 from E5Gui import E5MessageBox
 from E5Gui.E5ProgressDialog import E5ProgressDialog
@@ -88,27 +88,74 @@
             progress.setValue(tot)
             progress.deleteLater()
         return moduleDict
+    
+    def __findApplicationRoot(self):
+        """
+        Private method to find the application root path.
+        
+        @return application root path
+        @rtype str
+        """
+        candidates = []
+        path = self.project.getProjectPath()
+        init = os.path.join(path, "__init__.py")
+        if os.path.exists(init):
+            # project is a package
+            return path
+        else:
+            # check, if one of the top directories is a package
+            for entry in os.listdir(path):
+                if entry.startswith("."):
+                    # ignore hidden files and directories
+                    continue
+                
+                fullpath = os.path.join(path, entry)
+                if os.path.isdir(fullpath):
+                    init = os.path.join(fullpath, "__init__.py")
+                    if os.path.exists(init):
+                        candidates.append(fullpath)
+            
+            if len(candidates) == 1:
+                return candidates[0]
+            elif len(candidates) > 1:
+                root, ok = QInputDialog.getItem(
+                    None,
+                    self.tr("Application Diagram"),
+                    self.tr("Select the application directory:"),
+                    sorted(candidates),
+                    0, True)
+                if ok:
+                    return root
+            else:
+                E5MessageBox.warning(
+                    None,
+                    self.tr("Application Diagram"),
+                    self.tr("""No application package could be detected."""
+                            """ Aborting..."""))
+        return None
         
     def buildDiagram(self):
         """
         Public method to build the packages shapes of the diagram.
         """
-        project = (
-            os.path.splitdrive(self.project.getProjectPath())[1]
-            .replace(os.sep, '.')[1:]
-        )
+        rpath = self.__findApplicationRoot()
+        if rpath is None:
+            # no root path detected
+            return
+        
+        root = os.path.splitdrive(rpath)[1].replace(os.sep, '.')[1:]
+        
         packages = {}
-        shapes = {}
-        p = 10
-        y = 10
-        maxHeight = 0
-        sceneRect = self.umlView.sceneRect()
+        self.__shapes = {}
         
         modules = self.__buildModulesDict()
         
         # step 1: build a dictionary of packages
         for module in sorted(modules.keys()):
-            packageName, moduleName = module.rsplit(".", 1)
+            if "." in module:
+                packageName, moduleName = module.rsplit(".", 1)
+            else:
+                packageName, moduleName = "", module
             if packageName in packages:
                 packages[packageName][0].append(moduleName)
             else:
@@ -128,14 +175,14 @@
                         if n in modules:
                             impLst.append(n)
                         else:
-                            n = "{0}.{1}".format(project, moduleImport)
+                            n = "{0}.{1}".format(root, moduleImport)
                             if n in modules:
                                 impLst.append(n)
                             elif n in packages:
                                 n = "{0}.<<Dummy>>".format(n)
                                 impLst.append(n)
                     else:
-                        n = "{0}.{1}".format(project, moduleImport)
+                        n = "{0}.{1}".format(root, moduleImport)
                         if n in modules:
                             impLst.append(n)
             for moduleImport in list(modules[module].from_imports.keys()):
@@ -170,27 +217,30 @@
                         if n in modules:
                             impLst.append(n)
                         else:
-                            n = "{0}.{1}".format(project, moduleImport)
+                            n = "{0}.{1}".format(root, moduleImport)
                             if n in modules:
                                 impLst.append(n)
                             elif n in packages:
                                 n = "{0}.<<Dummy>>".format(n)
                                 impLst.append(n)
                     else:
-                        n = "{0}.{1}".format(project, moduleImport)
+                        n = "{0}.{1}".format(root, moduleImport)
                         if n in modules:
                             impLst.append(n)
             for moduleImport in impLst:
                 impPackage = moduleImport.rsplit(".", 1)[0]
-                if (
-                    impPackage not in packages[package][1] and
-                    impPackage != package
-                ):
-                    packages[package][1].append(impPackage)
-                    
+                try:
+                    if (
+                        impPackage not in packages[package][1] and
+                        impPackage != package
+                    ):
+                        packages[package][1].append(impPackage)
+                except KeyError:
+                    continue
+        
         for package in sorted(packages.keys()):
             if package:
-                relPackage = package.replace(project, '')
+                relPackage = package.replace(root, '')
                 if relPackage and relPackage[0] == '.':
                     relPackage = relPackage[1:]
                 else:
@@ -199,31 +249,22 @@
                 relPackage = self.tr("<<Others>>")
             shape = self.__addPackage(
                 relPackage, packages[package][0], 0.0, 0.0)
-            shapeRect = shape.sceneBoundingRect()
-            shapes[package] = (shape, packages[package][1])
-            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[package] = (shape, packages[package][1])
         
-        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 __addPackage(self, name, modules, x, y):
         """
         Private method to add a package to the diagram.
@@ -242,21 +283,101 @@
                          colors=self.umlView.getDrawingColors())
         pw.setId(self.umlView.getItemId())
         return pw
+    
+    def __arrangeNodes(self, nodes, routes, whiteSpaceFactor=1.2):
+        """
+        Private method to arrange the shapes on the canvas.
         
-    def __createAssociations(self, shapes):
+        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
         """
-        Private method to generate the associations between the package shapes.
+        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)
         
-        @param shapes list of shapes
+        # 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 routes list of associations
+        @type list of tuple of (str, str)
         """
         from .AssociationItem import AssociationItem, AssociationType
-        for package in shapes:
-            for rel in shapes[package][1]:
-                assoc = AssociationItem(
-                    shapes[package][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):
         """

eric ide

mercurial