--- 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, ""