eric6/Graphics/ApplicationDiagramBuilder.py

changeset 8286
62ae22eae123
parent 8270
6ba3564b7161
child 8287
30eb7bc13d63
equal deleted inserted replaced
8285:1816b622aef2 8286:62ae22eae123
8 """ 8 """
9 9
10 import os 10 import os
11 import glob 11 import glob
12 12
13 from PyQt5.QtWidgets import QApplication 13 from PyQt5.QtWidgets import QApplication, QInputDialog
14 14
15 from E5Gui import E5MessageBox 15 from E5Gui import E5MessageBox
16 from E5Gui.E5ProgressDialog import E5ProgressDialog 16 from E5Gui.E5ProgressDialog import E5ProgressDialog
17 17
18 from .UMLDiagramBuilder import UMLDiagramBuilder 18 from .UMLDiagramBuilder import UMLDiagramBuilder
86 moduleDict[name] = mod 86 moduleDict[name] = mod
87 finally: 87 finally:
88 progress.setValue(tot) 88 progress.setValue(tot)
89 progress.deleteLater() 89 progress.deleteLater()
90 return moduleDict 90 return moduleDict
91
92 def __findApplicationRoot(self):
93 """
94 Private method to find the application root path.
95
96 @return application root path
97 @rtype str
98 """
99 candidates = []
100 path = self.project.getProjectPath()
101 init = os.path.join(path, "__init__.py")
102 if os.path.exists(init):
103 # project is a package
104 return path
105 else:
106 # check, if one of the top directories is a package
107 for entry in os.listdir(path):
108 if entry.startswith("."):
109 # ignore hidden files and directories
110 continue
111
112 fullpath = os.path.join(path, entry)
113 if os.path.isdir(fullpath):
114 init = os.path.join(fullpath, "__init__.py")
115 if os.path.exists(init):
116 candidates.append(fullpath)
117
118 if len(candidates) == 1:
119 return candidates[0]
120 elif len(candidates) > 1:
121 root, ok = QInputDialog.getItem(
122 None,
123 self.tr("Application Diagram"),
124 self.tr("Select the application directory:"),
125 sorted(candidates),
126 0, True)
127 if ok:
128 return root
129 else:
130 E5MessageBox.warning(
131 None,
132 self.tr("Application Diagram"),
133 self.tr("""No application package could be detected."""
134 """ Aborting..."""))
135 return None
91 136
92 def buildDiagram(self): 137 def buildDiagram(self):
93 """ 138 """
94 Public method to build the packages shapes of the diagram. 139 Public method to build the packages shapes of the diagram.
95 """ 140 """
96 project = ( 141 rpath = self.__findApplicationRoot()
97 os.path.splitdrive(self.project.getProjectPath())[1] 142 if rpath is None:
98 .replace(os.sep, '.')[1:] 143 # no root path detected
99 ) 144 return
145
146 root = os.path.splitdrive(rpath)[1].replace(os.sep, '.')[1:]
147
100 packages = {} 148 packages = {}
101 shapes = {} 149 self.__shapes = {}
102 p = 10
103 y = 10
104 maxHeight = 0
105 sceneRect = self.umlView.sceneRect()
106 150
107 modules = self.__buildModulesDict() 151 modules = self.__buildModulesDict()
108 152
109 # step 1: build a dictionary of packages 153 # step 1: build a dictionary of packages
110 for module in sorted(modules.keys()): 154 for module in sorted(modules.keys()):
111 packageName, moduleName = module.rsplit(".", 1) 155 if "." in module:
156 packageName, moduleName = module.rsplit(".", 1)
157 else:
158 packageName, moduleName = "", module
112 if packageName in packages: 159 if packageName in packages:
113 packages[packageName][0].append(moduleName) 160 packages[packageName][0].append(moduleName)
114 else: 161 else:
115 packages[packageName] = ([moduleName], []) 162 packages[packageName] = ([moduleName], [])
116 163
126 n = "{0}.{1}".format(modules[module].package, 173 n = "{0}.{1}".format(modules[module].package,
127 moduleImport) 174 moduleImport)
128 if n in modules: 175 if n in modules:
129 impLst.append(n) 176 impLst.append(n)
130 else: 177 else:
131 n = "{0}.{1}".format(project, moduleImport) 178 n = "{0}.{1}".format(root, moduleImport)
132 if n in modules: 179 if n in modules:
133 impLst.append(n) 180 impLst.append(n)
134 elif n in packages: 181 elif n in packages:
135 n = "{0}.<<Dummy>>".format(n) 182 n = "{0}.<<Dummy>>".format(n)
136 impLst.append(n) 183 impLst.append(n)
137 else: 184 else:
138 n = "{0}.{1}".format(project, moduleImport) 185 n = "{0}.{1}".format(root, moduleImport)
139 if n in modules: 186 if n in modules:
140 impLst.append(n) 187 impLst.append(n)
141 for moduleImport in list(modules[module].from_imports.keys()): 188 for moduleImport in list(modules[module].from_imports.keys()):
142 if moduleImport.startswith('.'): 189 if moduleImport.startswith('.'):
143 dots = len(moduleImport) - len(moduleImport.lstrip('.')) 190 dots = len(moduleImport) - len(moduleImport.lstrip('.'))
168 n = "{0}.{1}".format(modules[module].package, 215 n = "{0}.{1}".format(modules[module].package,
169 moduleImport) 216 moduleImport)
170 if n in modules: 217 if n in modules:
171 impLst.append(n) 218 impLst.append(n)
172 else: 219 else:
173 n = "{0}.{1}".format(project, moduleImport) 220 n = "{0}.{1}".format(root, moduleImport)
174 if n in modules: 221 if n in modules:
175 impLst.append(n) 222 impLst.append(n)
176 elif n in packages: 223 elif n in packages:
177 n = "{0}.<<Dummy>>".format(n) 224 n = "{0}.<<Dummy>>".format(n)
178 impLst.append(n) 225 impLst.append(n)
179 else: 226 else:
180 n = "{0}.{1}".format(project, moduleImport) 227 n = "{0}.{1}".format(root, moduleImport)
181 if n in modules: 228 if n in modules:
182 impLst.append(n) 229 impLst.append(n)
183 for moduleImport in impLst: 230 for moduleImport in impLst:
184 impPackage = moduleImport.rsplit(".", 1)[0] 231 impPackage = moduleImport.rsplit(".", 1)[0]
185 if ( 232 try:
186 impPackage not in packages[package][1] and 233 if (
187 impPackage != package 234 impPackage not in packages[package][1] and
188 ): 235 impPackage != package
189 packages[package][1].append(impPackage) 236 ):
190 237 packages[package][1].append(impPackage)
238 except KeyError:
239 continue
240
191 for package in sorted(packages.keys()): 241 for package in sorted(packages.keys()):
192 if package: 242 if package:
193 relPackage = package.replace(project, '') 243 relPackage = package.replace(root, '')
194 if relPackage and relPackage[0] == '.': 244 if relPackage and relPackage[0] == '.':
195 relPackage = relPackage[1:] 245 relPackage = relPackage[1:]
196 else: 246 else:
197 relPackage = self.tr("<<Application>>") 247 relPackage = self.tr("<<Application>>")
198 else: 248 else:
199 relPackage = self.tr("<<Others>>") 249 relPackage = self.tr("<<Others>>")
200 shape = self.__addPackage( 250 shape = self.__addPackage(
201 relPackage, packages[package][0], 0.0, 0.0) 251 relPackage, packages[package][0], 0.0, 0.0)
202 shapeRect = shape.sceneBoundingRect() 252 self.__shapes[package] = (shape, packages[package][1])
203 shapes[package] = (shape, packages[package][1]) 253
204 pn = p + shapeRect.width() + 10 254 # build a list of routes
205 maxHeight = max(maxHeight, shapeRect.height()) 255 nodes = []
206 if pn > sceneRect.width(): 256 routes = []
207 p = 10 257 for module in self.__shapes:
208 y += maxHeight + 10 258 nodes.append(module)
209 maxHeight = shapeRect.height() 259 for rel in self.__shapes[module][1]:
210 shape.setPos(p, y) 260 route = (module, rel)
211 p += shapeRect.width() + 10 261 if route not in routes:
212 else: 262 routes.append(route)
213 shape.setPos(p, y) 263
214 p = pn 264 self.__arrangeNodes(nodes, routes[:])
215 265 self.__createAssociations(routes)
216 rect = self.umlView._getDiagramRect(10)
217 sceneRect = self.umlView.sceneRect()
218 if rect.width() > sceneRect.width():
219 sceneRect.setWidth(rect.width())
220 if rect.height() > sceneRect.height():
221 sceneRect.setHeight(rect.height())
222 self.umlView.setSceneSize(sceneRect.width(), sceneRect.height())
223
224 self.__createAssociations(shapes)
225 self.umlView.autoAdjustSceneSize(limit=True) 266 self.umlView.autoAdjustSceneSize(limit=True)
226 267
227 def __addPackage(self, name, modules, x, y): 268 def __addPackage(self, name, modules, x, y):
228 """ 269 """
229 Private method to add a package to the diagram. 270 Private method to add a package to the diagram.
230 271
231 @param name package name to be shown (string) 272 @param name package name to be shown (string)
240 pm = PackageModel(name, modules) 281 pm = PackageModel(name, modules)
241 pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene, 282 pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene,
242 colors=self.umlView.getDrawingColors()) 283 colors=self.umlView.getDrawingColors())
243 pw.setId(self.umlView.getItemId()) 284 pw.setId(self.umlView.getItemId())
244 return pw 285 return pw
245 286
246 def __createAssociations(self, shapes): 287 def __arrangeNodes(self, nodes, routes, whiteSpaceFactor=1.2):
247 """ 288 """
248 Private method to generate the associations between the package shapes. 289 Private method to arrange the shapes on the canvas.
249 290
250 @param shapes list of shapes 291 The algorithm is borrowed from Boa Constructor.
292
293 @param nodes list of nodes to arrange
294 @type list of str
295 @param routes list of routes
296 @type list of tuple of (str, str)
297 @param whiteSpaceFactor factor to increase whitespace between
298 items
299 @type float
300 """
301 from . import GraphicsUtilities
302 generations = GraphicsUtilities.sort(nodes, routes)
303
304 # calculate width and height of all elements
305 sizes = []
306 for generation in generations:
307 sizes.append([])
308 for child in generation:
309 sizes[-1].append(
310 self.__shapes[child][0].sceneBoundingRect())
311
312 # calculate total width and total height
313 width = 0
314 height = 0
315 widths = []
316 heights = []
317 for generation in sizes:
318 currentWidth = 0
319 currentHeight = 0
320
321 for rect in generation:
322 if rect.height() > currentHeight:
323 currentHeight = rect.height()
324 currentWidth += rect.width()
325
326 # update totals
327 if currentWidth > width:
328 width = currentWidth
329 height += currentHeight
330
331 # store generation info
332 widths.append(currentWidth)
333 heights.append(currentHeight)
334
335 # add in some whitespace
336 width *= whiteSpaceFactor
337 height = height * whiteSpaceFactor - 20
338 verticalWhiteSpace = 40.0
339
340 sceneRect = self.umlView.sceneRect()
341 width += 50.0
342 height += 50.0
343 swidth = sceneRect.width() if width < sceneRect.width() else width
344 sheight = sceneRect.height() if height < sceneRect.height() else height
345 self.umlView.setSceneSize(swidth, sheight)
346
347 # distribute each generation across the width and the
348 # generations across height
349 y = 10.0
350 for currentWidth, currentHeight, generation in (
351 zip(reversed(widths), reversed(heights), reversed(generations))
352 ):
353 x = 10.0
354 # whiteSpace is the space between any two elements
355 whiteSpace = (
356 (width - currentWidth - 20) /
357 (len(generation) - 1.0 or 2.0)
358 )
359 for name in generation:
360 shape = self.__shapes[name][0]
361 shape.setPos(x, y)
362 rect = shape.sceneBoundingRect()
363 x = x + rect.width() + whiteSpace
364 y = y + currentHeight + verticalWhiteSpace
365
366 def __createAssociations(self, routes):
367 """
368 Private method to generate the associations between the module shapes.
369
370 @param routes list of associations
371 @type list of tuple of (str, str)
251 """ 372 """
252 from .AssociationItem import AssociationItem, AssociationType 373 from .AssociationItem import AssociationItem, AssociationType
253 for package in shapes: 374 for route in routes:
254 for rel in shapes[package][1]: 375 assoc = AssociationItem(
255 assoc = AssociationItem( 376 self.__shapes[route[0]][0],
256 shapes[package][0], shapes[rel][0], 377 self.__shapes[route[1]][0],
257 AssociationType.IMPORTS, 378 AssociationType.IMPORTS,
258 colors=self.umlView.getDrawingColors()) 379 colors=self.umlView.getDrawingColors())
259 self.scene.addItem(assoc) 380 self.scene.addItem(assoc)
260 381
261 def getPersistenceData(self): 382 def getPersistenceData(self):
262 """ 383 """
263 Public method to get a string for data to be persisted. 384 Public method to get a string for data to be persisted.
264 385

eric ide

mercurial