27 """ |
27 """ |
28 def __init__(self, dialog, view, project, package, noAttrs=False): |
28 def __init__(self, dialog, view, project, package, noAttrs=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 @param package name of a python package to be shown (string) |
35 @type UMLGraphicsView |
|
36 @param project reference to the project object |
|
37 @type Project |
|
38 @param package name of a python package to be shown |
|
39 @type str |
36 @param noAttrs flag indicating, that no attributes should be shown |
40 @param noAttrs flag indicating, that no attributes should be shown |
37 (boolean) |
41 @type bool |
38 """ |
42 """ |
39 super().__init__(dialog, view, project) |
43 super().__init__(dialog, view, project) |
40 self.setObjectName("PackageDiagram") |
44 self.setObjectName("PackageDiagram") |
41 |
45 |
42 self.package = os.path.abspath(package) |
46 self.package = os.path.abspath(package) |
43 self.noAttrs = noAttrs |
47 self.noAttrs = noAttrs |
|
48 |
|
49 self.__relPackage = ( |
|
50 self.project.getRelativePath(self.package) |
|
51 if self.project.isProjectSource(self.package) else |
|
52 "" |
|
53 ) |
44 |
54 |
45 def initialize(self): |
55 def initialize(self): |
46 """ |
56 """ |
47 Public method to initialize the object. |
57 Public method to initialize the object. |
48 """ |
58 """ |
57 |
67 |
58 def __getCurrentShape(self, name): |
68 def __getCurrentShape(self, name): |
59 """ |
69 """ |
60 Private method to get the named shape. |
70 Private method to get the named shape. |
61 |
71 |
62 @param name name of the shape (string) |
72 @param name name of the shape |
63 @return shape (QCanvasItem) |
73 @type str |
|
74 @return shape |
|
75 @rtype QCanvasItem |
64 """ |
76 """ |
65 return self.allClasses.get(name) |
77 return self.allClasses.get(name) |
66 |
78 |
67 def __buildModulesDict(self): |
79 def __buildModulesDict(self): |
68 """ |
80 """ |
69 Private method to build a dictionary of modules contained in the |
81 Private method to build a dictionary of modules contained in the |
70 package. |
82 package. |
71 |
83 |
72 @return dictionary of modules contained in the package. |
84 @return dictionary of modules contained in the package |
|
85 @rtype dict |
73 """ |
86 """ |
74 import Utilities.ModuleParser |
87 import Utilities.ModuleParser |
75 |
88 |
76 supportedExt = ( |
89 supportedExt = ( |
77 ['*{0}'.format(ext) for ext in |
90 ['*{0}'.format(ext) for ext in |
206 self.tr("The directory <b>'{0}'</b> is not a package.") |
220 self.tr("The directory <b>'{0}'</b> is not a package.") |
207 .format(self.package)) |
221 .format(self.package)) |
208 return |
222 return |
209 |
223 |
210 modules = self.__buildModulesDict() |
224 modules = self.__buildModulesDict() |
211 if not modules: |
225 subpackages = self.__buildSubpackagesDict() |
|
226 |
|
227 if not modules and not subpackages: |
212 ct = QGraphicsTextItem(None) |
228 ct = QGraphicsTextItem(None) |
213 self.scene.addItem(ct) |
229 self.scene.addItem(ct) |
214 ct.setHtml( |
230 ct.setHtml(self.buildErrorMessage( |
215 self.tr( |
231 self.tr("The package <b>'{0}'</b> does not contain any modules" |
216 "The package <b>'{0}'</b> does not contain any modules.") |
232 " or subpackages.").format(self.package) |
217 .format(self.package)) |
233 )) |
218 return |
234 return |
219 |
235 |
220 # step 1: build all classes found in the modules |
236 # step 1: build all classes found in the modules |
221 classesFound = False |
237 classesFound = False |
222 |
238 |
223 for modName in list(modules.keys()): |
239 for modName in list(modules.keys()): |
224 module = modules[modName] |
240 module = modules[modName] |
225 for cls in list(module.classes.keys()): |
241 for cls in list(module.classes.keys()): |
226 classesFound = True |
242 classesFound = True |
227 self.__addLocalClass(cls, module.classes[cls], 0, 0) |
243 self.__addLocalClass(cls, module.classes[cls], 0, 0) |
228 if not classesFound: |
244 if not classesFound and not subpackages: |
229 ct = QGraphicsTextItem(None) |
245 ct = QGraphicsTextItem(None) |
230 self.scene.addItem(ct) |
246 self.scene.addItem(ct) |
231 ct.setHtml( |
247 ct.setHtml(self.buildErrorMessage( |
232 self.tr( |
248 self.tr("The package <b>'{0}'</b> does not contain any" |
233 "The package <b>'{0}'</b> does not contain any classes.") |
249 " classes or subpackages.").format(self.package) |
234 .format(self.package)) |
250 )) |
235 return |
251 return |
236 |
252 |
237 # step 2: build the class hierarchies |
253 # step 2: build the class hierarchies |
238 routes = [] |
254 routes = [] |
239 nodes = [] |
255 nodes = [] |
241 for modName in list(modules.keys()): |
257 for modName in list(modules.keys()): |
242 module = modules[modName] |
258 module = modules[modName] |
243 todo = [module.createHierarchy()] |
259 todo = [module.createHierarchy()] |
244 while todo: |
260 while todo: |
245 hierarchy = todo[0] |
261 hierarchy = todo[0] |
246 for className in list(hierarchy.keys()): |
262 for className in hierarchy: |
247 cw = self.__getCurrentShape(className) |
263 cw = self.__getCurrentShape(className) |
248 if not cw and className.find('.') >= 0: |
264 if not cw and className.find('.') >= 0: |
249 cw = self.__getCurrentShape(className.split('.')[-1]) |
265 cw = self.__getCurrentShape(className.split('.')[-1]) |
250 if cw: |
266 if cw: |
251 self.allClasses[className] = cw |
267 self.allClasses[className] = cw |
280 routes.append((className, child)) |
296 routes.append((className, child)) |
281 |
297 |
282 del todo[0] |
298 del todo[0] |
283 |
299 |
284 # step 3: build the subpackages |
300 # step 3: build the subpackages |
285 subpackages = self.__buildSubpackagesDict() |
|
286 for subpackage in sorted(subpackages.keys()): |
301 for subpackage in sorted(subpackages.keys()): |
287 self.__addPackage(subpackage, subpackages[subpackage], 0, 0) |
302 self.__addPackage(subpackage, subpackages[subpackage], 0, 0) |
288 nodes.append(subpackage) |
303 nodes.append(subpackage) |
289 |
304 |
290 self.__arrangeClasses(nodes, routes[:]) |
305 self.__arrangeClasses(nodes, routes[:]) |
296 Private method to arrange the shapes on the canvas. |
311 Private method to arrange the shapes on the canvas. |
297 |
312 |
298 The algorithm is borrowed from Boa Constructor. |
313 The algorithm is borrowed from Boa Constructor. |
299 |
314 |
300 @param nodes list of nodes to arrange |
315 @param nodes list of nodes to arrange |
|
316 @type list of str |
301 @param routes list of routes |
317 @param routes list of routes |
|
318 @type list of tuple of (str, str) |
302 @param whiteSpaceFactor factor to increase whitespace between |
319 @param whiteSpaceFactor factor to increase whitespace between |
303 items (float) |
320 items |
|
321 @type float |
304 """ |
322 """ |
305 from . import GraphicsUtilities |
323 from . import GraphicsUtilities |
306 generations = GraphicsUtilities.sort(nodes, routes) |
324 generations = GraphicsUtilities.sort(nodes, routes) |
307 |
325 |
308 # calculate width and height of all elements |
326 # calculate width and height of all elements |
333 height += currentHeight |
351 height += currentHeight |
334 |
352 |
335 # store generation info |
353 # store generation info |
336 widths.append(currentWidth) |
354 widths.append(currentWidth) |
337 heights.append(currentHeight) |
355 heights.append(currentHeight) |
338 |
356 |
339 # add in some whitespace |
357 # add in some whitespace |
340 width *= whiteSpaceFactor |
358 width *= whiteSpaceFactor |
341 height = height * whiteSpaceFactor - 20 |
359 height = height * whiteSpaceFactor - 20 |
342 verticalWhiteSpace = 40.0 |
360 verticalWhiteSpace = 40.0 |
343 |
361 |
344 sceneRect = self.umlView.sceneRect() |
362 sceneRect = self.umlView.sceneRect() |
345 width += 50.0 |
363 width += 50.0 |
346 height += 50.0 |
364 height += 50.0 |
347 swidth = width < sceneRect.width() and sceneRect.width() or width |
365 swidth = sceneRect.width() if width < sceneRect.width() else width |
348 sheight = height < sceneRect.height() and sceneRect.height() or height |
366 sheight = sceneRect.height() if height < sceneRect.height() else height |
349 self.umlView.setSceneSize(swidth, sheight) |
367 self.umlView.setSceneSize(swidth, sheight) |
350 |
368 |
351 # distribute each generation across the width and the |
369 # distribute each generation across the width and the |
352 # generations across height |
370 # generations across height |
353 y = 10.0 |
371 y = 10.0 |
369 |
387 |
370 def __addLocalClass(self, className, _class, x, y, isRbModule=False): |
388 def __addLocalClass(self, className, _class, x, y, isRbModule=False): |
371 """ |
389 """ |
372 Private method to add a class defined in the module. |
390 Private method to add a class defined in the module. |
373 |
391 |
374 @param className name of the class to be as a dictionary key (string) |
392 @param className name of the class to be as a dictionary key |
375 @param _class class to be shown (ModuleParser.Class) |
393 @type str |
376 @param x x-coordinate (float) |
394 @param _class class to be shown |
377 @param y y-coordinate (float) |
395 @type ModuleParser.Class |
378 @param isRbModule flag indicating a Ruby module (boolean) |
396 @param x x-coordinate |
|
397 @type float |
|
398 @param y y-coordinate |
|
399 @type float |
|
400 @param isRbModule flag indicating a Ruby module |
|
401 @type bool |
379 """ |
402 """ |
380 from .ClassItem import ClassItem, ClassModel |
403 from .ClassItem import ClassItem, ClassModel |
381 meths = sorted(_class.methods.keys()) |
|
382 attrs = sorted(_class.attributes.keys()) |
|
383 name = _class.name |
404 name = _class.name |
384 if isRbModule: |
405 if isRbModule: |
385 name = "{0} (Module)".format(name) |
406 name = "{0} (Module)".format(name) |
386 cl = ClassModel(name, meths[:], attrs[:]) |
407 cl = ClassModel( |
|
408 name, |
|
409 sorted(_class.methods.keys())[:], |
|
410 sorted(_class.attributes.keys())[:], |
|
411 sorted(_class.globals.keys())[:] |
|
412 ) |
387 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene, |
413 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene, |
388 colors=self.umlView.getDrawingColors()) |
414 colors=self.umlView.getDrawingColors()) |
389 cw.setId(self.umlView.getItemId()) |
415 cw.setId(self.umlView.getItemId()) |
390 self.allClasses[className] = cw |
416 self.allClasses[className] = cw |
391 |
417 |
394 Private method to add a class defined outside the module. |
420 Private method to add a class defined outside the module. |
395 |
421 |
396 If the canvas is too small to take the shape, it |
422 If the canvas is too small to take the shape, it |
397 is enlarged. |
423 is enlarged. |
398 |
424 |
399 @param _class class to be shown (string) |
425 @param _class class to be shown |
400 @param x x-coordinate (float) |
426 @type ModuleParser.Class |
401 @param y y-coordinate (float) |
427 @param x x-coordinate |
|
428 @type float |
|
429 @param y y-coordinate |
|
430 @type float |
402 """ |
431 """ |
403 from .ClassItem import ClassItem, ClassModel |
432 from .ClassItem import ClassItem, ClassModel |
404 cl = ClassModel(_class) |
433 cl = ClassModel(_class) |
405 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene, |
434 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene, |
406 colors=self.umlView.getDrawingColors()) |
435 colors=self.umlView.getDrawingColors()) |
409 |
438 |
410 def __addPackage(self, name, modules, x, y): |
439 def __addPackage(self, name, modules, x, y): |
411 """ |
440 """ |
412 Private method to add a package to the diagram. |
441 Private method to add a package to the diagram. |
413 |
442 |
414 @param name package name to be shown (string) |
443 @param name package name to be shown |
|
444 @type str |
415 @param modules list of module names contained in the package |
445 @param modules list of module names contained in the package |
416 (list of strings) |
446 @type list of str |
417 @param x x-coordinate (float) |
447 @param x x-coordinate |
418 @param y y-coordinate (float) |
448 @type float |
|
449 @param y y-coordinate |
|
450 @type float |
419 """ |
451 """ |
420 from .PackageItem import PackageItem, PackageModel |
452 from .PackageItem import PackageItem, PackageModel |
421 pm = PackageModel(name, modules) |
453 pm = PackageModel(name, modules) |
422 pw = PackageItem(pm, x, y, scene=self.scene, |
454 pw = PackageItem(pm, x, y, scene=self.scene, |
423 colors=self.umlView.getDrawingColors()) |
455 colors=self.umlView.getDrawingColors()) |
427 def __createAssociations(self, routes): |
459 def __createAssociations(self, routes): |
428 """ |
460 """ |
429 Private method to generate the associations between the class shapes. |
461 Private method to generate the associations between the class shapes. |
430 |
462 |
431 @param routes list of relationsships |
463 @param routes list of relationsships |
|
464 @type list of tuple of (str, str) |
432 """ |
465 """ |
433 from .AssociationItem import AssociationItem, AssociationType |
466 from .AssociationItem import AssociationItem, AssociationType |
434 for route in routes: |
467 for route in routes: |
435 if len(route) > 1: |
468 if len(route) > 1: |
436 assoc = AssociationItem( |
469 assoc = AssociationItem( |
443 |
476 |
444 def getPersistenceData(self): |
477 def getPersistenceData(self): |
445 """ |
478 """ |
446 Public method to get a string for data to be persisted. |
479 Public method to get a string for data to be persisted. |
447 |
480 |
448 @return persisted data string (string) |
481 @return persisted data string |
|
482 @rtype str |
449 """ |
483 """ |
450 return "package={0}, no_attributes={1}".format( |
484 return "package={0}, no_attributes={1}".format( |
451 self.package, self.noAttrs) |
485 self.package, self.noAttrs) |
452 |
486 |
453 def parsePersistenceData(self, version, data): |
487 def parsePersistenceData(self, version, data): |
454 """ |
488 """ |
455 Public method to parse persisted data. |
489 Public method to parse persisted data. |
456 |
490 |
457 @param version version of the data (string) |
491 @param version version of the data |
458 @param data persisted data to be parsed (string) |
492 @type str |
459 @return flag indicating success (boolean) |
493 @param data persisted data to be parsed |
|
494 @type str |
|
495 @return flag indicating success |
|
496 @rtype bool |
460 """ |
497 """ |
461 parts = data.split(", ") |
498 parts = data.split(", ") |
462 if ( |
499 if ( |
463 len(parts) != 2 or |
500 len(parts) != 2 or |
464 not parts[0].startswith("package=") or |
501 not parts[0].startswith("package=") or |
470 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip()) |
507 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip()) |
471 |
508 |
472 self.initialize() |
509 self.initialize() |
473 |
510 |
474 return True |
511 return True |
|
512 |
|
513 def toDict(self): |
|
514 """ |
|
515 Public method to collect data to be persisted. |
|
516 |
|
517 @return dictionary containing data to be persisted |
|
518 @rtype dict |
|
519 """ |
|
520 data = { |
|
521 "project_name": self.project.getProjectName(), |
|
522 "no_attributes": self.noAttrs, |
|
523 } |
|
524 |
|
525 data["package"] = ( |
|
526 Utilities.fromNativeSeparators(self.__relPackage) |
|
527 if self.__relPackage else |
|
528 Utilities.fromNativeSeparators(self.package) |
|
529 ) |
|
530 |
|
531 return data |
|
532 |
|
533 def fromDict(self, version, data): |
|
534 """ |
|
535 Public method to populate the class with data persisted by 'toDict()'. |
|
536 |
|
537 @param version version of the data |
|
538 @type str |
|
539 @param data dictionary containing the persisted data |
|
540 @type dict |
|
541 @return tuple containing a flag indicating success and an info |
|
542 message in case the diagram belongs to a different project |
|
543 @rtype tuple of (bool, str) |
|
544 """ |
|
545 try: |
|
546 self.noAttrs = data["no_attributes"] |
|
547 |
|
548 package = Utilities.toNativeSeparators(data["package"]) |
|
549 if os.path.isabs(package): |
|
550 self.package = package |
|
551 self.__relPackage = "" |
|
552 else: |
|
553 # relative package paths indicate a project package |
|
554 if data["project_name"] != self.project.getProjectName(): |
|
555 msg = self.tr( |
|
556 "<p>The diagram belongs to project <b>{0}</b>." |
|
557 " Please open it and try again.</p>" |
|
558 ).format(data["project_name"]) |
|
559 return False, msg |
|
560 |
|
561 self.__relPackage = package |
|
562 self.package = self.project.getAbsolutePath(package) |
|
563 except KeyError: |
|
564 return False, "" |
|
565 |
|
566 self.initialize() |
|
567 |
|
568 return True, "" |