--- 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): """