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