eric6/Graphics/ApplicationDiagramBuilder.py

branch
maintenance
changeset 8400
b3eefd7e58d1
parent 8273
698ae46f40a4
parent 8295
3f5e8b0a338e
equal deleted inserted replaced
8274:197414ba11cc 8400:b3eefd7e58d1
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
27 """ 27 """
28 def __init__(self, dialog, view, project, noModules=False): 28 def __init__(self, dialog, view, project, noModules=False):
29 """ 29 """
30 Constructor 30 Constructor
31 31
32 @param dialog reference to the UML dialog (UMLDialog) 32 @param dialog reference to the UML dialog
33 @param view reference to the view object (UMLGraphicsView) 33 @type UMLDialog
34 @param project reference to the project object (Project) 34 @param view reference to the view object
35 @type UMLGraphicsView
36 @param project reference to the project object
37 @type Project
35 @param noModules flag indicating, that no module names should be 38 @param noModules flag indicating, that no module names should be
36 shown (boolean) 39 shown
40 @type bool
37 """ 41 """
38 super().__init__(dialog, view, project) 42 super().__init__(dialog, view, project)
39 self.setObjectName("ApplicationDiagram") 43 self.setObjectName("ApplicationDiagram")
40 44
41 self.noModules = noModules 45 self.noModules = noModules
47 def __buildModulesDict(self): 51 def __buildModulesDict(self):
48 """ 52 """
49 Private method to build a dictionary of modules contained in the 53 Private method to build a dictionary of modules contained in the
50 application. 54 application.
51 55
52 @return dictionary of modules contained in the application. 56 @return dictionary of modules contained in the application
57 @rtype dict
53 """ 58 """
54 import Utilities.ModuleParser 59 import Utilities.ModuleParser
55 extensions = ( 60 extensions = (
56 Preferences.getPython("Python3Extensions") + 61 Preferences.getPython("Python3Extensions") +
57 ['.rb'] 62 ['.rb']
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
126 n = "{0}.{1}".format(modules[module].package, 178 n = "{0}.{1}".format(modules[module].package,
127 moduleImport) 179 moduleImport)
128 if n in modules: 180 if n in modules:
129 impLst.append(n) 181 impLst.append(n)
130 else: 182 else:
131 n = "{0}.{1}".format(project, moduleImport) 183 n = "{0}.{1}".format(root, moduleImport)
132 if n in modules: 184 if n in modules:
133 impLst.append(n) 185 impLst.append(n)
134 elif n in packages: 186 elif n in packages:
135 n = "{0}.<<Dummy>>".format(n) 187 n = "{0}.<<Dummy>>".format(n)
136 impLst.append(n) 188 impLst.append(n)
137 else: 189 else:
138 n = "{0}.{1}".format(project, moduleImport) 190 n = "{0}.{1}".format(root, moduleImport)
139 if n in modules: 191 if n in modules:
140 impLst.append(n) 192 impLst.append(n)
141 for moduleImport in list(modules[module].from_imports.keys()): 193 for moduleImport in list(modules[module].from_imports.keys()):
142 if moduleImport.startswith('.'): 194 if moduleImport.startswith('.'):
143 dots = len(moduleImport) - len(moduleImport.lstrip('.')) 195 dots = len(moduleImport) - len(moduleImport.lstrip('.'))
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
298 self.noModules = Utilities.toBool(parts[1].split("=", 1)[1].strip()) 432 self.noModules = Utilities.toBool(parts[1].split("=", 1)[1].strip())
299 433
300 self.initialize() 434 self.initialize()
301 435
302 return True 436 return True
437
438 def toDict(self):
439 """
440 Public method to collect data to be persisted.
441
442 @return dictionary containing data to be persisted
443 @rtype dict
444 """
445 return {
446 "project_name": self.project.getProjectName(),
447 "no_modules": self.noModules,
448 }
449
450 def fromDict(self, version, data):
451 """
452 Public method to populate the class with data persisted by 'toDict()'.
453
454 @param version version of the data
455 @type str
456 @param data dictionary containing the persisted data
457 @type dict
458 @return tuple containing a flag indicating success and an info
459 message in case the diagram belongs to a different project
460 @rtype tuple of (bool, str)
461 """
462 try:
463 self.noModules = data["no_modules"]
464
465 if data["project_name"] != self.project.getProjectName():
466 msg = self.tr(
467 "<p>The diagram belongs to project <b>{0}</b>."
468 " Please open it and try again.</p>"
469 ).format(data["project_name"])
470 return False, msg
471 except KeyError:
472 return False, ""
473
474 self.initialize()
475
476 return True, ""

eric ide

mercurial