eric7/Graphics/ApplicationDiagramBuilder.py

branch
eric7
changeset 8312
800c432b34c8
parent 8295
3f5e8b0a338e
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Graphics/ApplicationDiagramBuilder.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,476 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog showing an imports diagram of the application.
+"""
+
+import os
+import glob
+
+from PyQt5.QtWidgets import QApplication, QInputDialog
+
+from E5Gui import E5MessageBox
+from E5Gui.E5ProgressDialog import E5ProgressDialog
+
+from .UMLDiagramBuilder import UMLDiagramBuilder
+
+import Utilities
+import Preferences
+
+
+class ApplicationDiagramBuilder(UMLDiagramBuilder):
+    """
+    Class implementing a builder for imports diagrams of the application.
+    """
+    def __init__(self, dialog, view, project, noModules=False):
+        """
+        Constructor
+        
+        @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 noModules flag indicating, that no module names should be
+            shown
+        @type bool
+        """
+        super().__init__(dialog, view, project)
+        self.setObjectName("ApplicationDiagram")
+        
+        self.noModules = noModules
+        
+        self.umlView.setDiagramName(
+            self.tr("Application Diagram {0}").format(
+                self.project.getProjectName()))
+        
+    def __buildModulesDict(self):
+        """
+        Private method to build a dictionary of modules contained in the
+        application.
+        
+        @return dictionary of modules contained in the application
+        @rtype dict
+        """
+        import Utilities.ModuleParser
+        extensions = (
+            Preferences.getPython("Python3Extensions") +
+            ['.rb']
+        )
+        moduleDict = {}
+        mods = self.project.pdata["SOURCES"]
+        modules = []
+        for module in mods:
+            modules.append(Utilities.normabsjoinpath(
+                self.project.ppath, module))
+        tot = len(modules)
+        progress = E5ProgressDialog(
+            self.tr("Parsing modules..."),
+            None, 0, tot, self.tr("%v/%m Modules"), self.parent())
+        progress.setWindowTitle(self.tr("Application Diagram"))
+        try:
+            progress.show()
+            QApplication.processEvents()
+            
+            for prog, module in enumerate(modules):
+                progress.setValue(prog)
+                QApplication.processEvents()
+                if module.endswith("__init__.py"):
+                    continue
+                try:
+                    mod = Utilities.ModuleParser.readModule(
+                        module, extensions=extensions, caching=False)
+                except ImportError:
+                    continue
+                else:
+                    name = mod.name
+                    moduleDict[name] = mod
+        finally:
+            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.
+        """
+        rpath = self.__findApplicationRoot()
+        if rpath is None:
+            # no root path detected
+            return
+        
+        root = os.path.splitdrive(rpath)[1].replace(os.sep, '.')[1:]
+        
+        packages = {}
+        self.__shapes = {}
+        
+        modules = self.__buildModulesDict()
+        
+        # step 1: build a dictionary of packages
+        for module in sorted(modules.keys()):
+            if "." in module:
+                packageName, moduleName = module.rsplit(".", 1)
+            else:
+                packageName, moduleName = "", module
+            if packageName in packages:
+                packages[packageName][0].append(moduleName)
+            else:
+                packages[packageName] = ([moduleName], [])
+                
+        # step 2: assign modules to dictionaries and update import relationship
+        for module in sorted(modules.keys()):
+            package = module.rsplit(".", 1)[0]
+            impLst = []
+            for moduleImport in modules[module].imports:
+                if moduleImport in modules:
+                    impLst.append(moduleImport)
+                else:
+                    if moduleImport.find('.') == -1:
+                        n = "{0}.{1}".format(modules[module].package,
+                                             moduleImport)
+                        if n in modules:
+                            impLst.append(n)
+                        else:
+                            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(root, moduleImport)
+                        if n in modules:
+                            impLst.append(n)
+            for moduleImport in list(modules[module].from_imports.keys()):
+                if moduleImport.startswith('.'):
+                    dots = len(moduleImport) - len(moduleImport.lstrip('.'))
+                    if dots == 1:
+                        moduleImport = moduleImport[1:]
+                    elif dots > 1:
+                        packagePath = os.path.dirname(modules[module].file)
+                        hasInit = True
+                        ppath = packagePath
+                        while hasInit:
+                            ppath = os.path.dirname(ppath)
+                            hasInit = len(glob.glob(os.path.join(
+                                ppath, '__init__.*'))) > 0
+                        shortPackage = (
+                            packagePath.replace(ppath, '')
+                            .replace(os.sep, '.')[1:]
+                        )
+                        packageList = shortPackage.split('.')[1:]
+                        packageListLen = len(packageList)
+                        moduleImport = '.'.join(
+                            packageList[:packageListLen - dots + 1] +
+                            [moduleImport[dots:]])
+                
+                if moduleImport in modules:
+                    impLst.append(moduleImport)
+                else:
+                    if moduleImport.find('.') == -1:
+                        n = "{0}.{1}".format(modules[module].package,
+                                             moduleImport)
+                        if n in modules:
+                            impLst.append(n)
+                        else:
+                            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(root, moduleImport)
+                        if n in modules:
+                            impLst.append(n)
+            for moduleImport in impLst:
+                impPackage = moduleImport.rsplit(".", 1)[0]
+                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(root, '')
+                if relPackage and relPackage[0] == '.':
+                    relPackage = relPackage[1:]
+                else:
+                    relPackage = self.tr("<<Application>>")
+            else:
+                relPackage = self.tr("<<Others>>")
+            shape = self.__addPackage(
+                relPackage, packages[package][0], 0.0, 0.0)
+            self.__shapes[package] = (shape, packages[package][1])
+        
+        # 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.__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.
+        
+        @param name package name to be shown
+        @type str
+        @param modules list of module names contained in the package
+        @type list of str
+        @param x x-coordinate
+        @type float
+        @param y y-coordinate
+        @type float
+        @return reference to the package item
+        @rtype PackageItem
+        """
+        from .PackageItem import PackageItem, PackageModel
+        modules.sort()
+        pm = PackageModel(name, modules)
+        pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene,
+                         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.
+        
+        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 routes list of associations
+        @type list of tuple of (str, str)
+        """
+        from .AssociationItem import AssociationItem, AssociationType
+        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
+        @rtype str
+        """
+        return "project={0}, no_modules={1}".format(
+            self.project.getProjectFile(), self.noModules)
+    
+    def parsePersistenceData(self, version, data):
+        """
+        Public method to parse persisted data.
+        
+        @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 (
+            len(parts) != 2 or
+            not parts[0].startswith("project=") or
+            not parts[1].startswith("no_modules=")
+        ):
+            return False
+        
+        projectFile = parts[0].split("=", 1)[1].strip()
+        if projectFile != self.project.getProjectFile():
+            res = E5MessageBox.yesNo(
+                None,
+                self.tr("Load Diagram"),
+                self.tr(
+                    """<p>The diagram belongs to the project <b>{0}</b>."""
+                    """ Shall this project be opened?</p>""").format(
+                    projectFile))
+            if res:
+                self.project.openProject(projectFile)
+            
+        self.noModules = Utilities.toBool(parts[1].split("=", 1)[1].strip())
+        
+        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,
+        }
+    
+    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, ""

eric ide

mercurial