eric6/Graphics/UMLClassDiagramBuilder.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing a UML like class diagram.
8 """
9
10 from __future__ import unicode_literals
11 try: # Py3
12 from itertools import zip_longest
13 except ImportError:
14 from itertools import izip_longest as zip_longest # __IGNORE_WARNING__
15
16 from PyQt5.QtWidgets import QGraphicsTextItem
17
18 import Utilities
19 import Preferences
20
21 from .UMLDiagramBuilder import UMLDiagramBuilder
22
23
24 class UMLClassDiagramBuilder(UMLDiagramBuilder):
25 """
26 Class implementing a builder for UML like class diagrams.
27 """
28 def __init__(self, dialog, view, project, file, noAttrs=False):
29 """
30 Constructor
31
32 @param dialog reference to the UML dialog (UMLDialog)
33 @param view reference to the view object (UMLGraphicsView)
34 @param project reference to the project object (Project)
35 @param file file name of a python module to be shown (string)
36 @keyparam noAttrs flag indicating, that no attributes should be shown
37 (boolean)
38 """
39 super(UMLClassDiagramBuilder, self).__init__(dialog, view, project)
40 self.setObjectName("UMLClassDiagramBuilder")
41
42 self.file = file
43 self.noAttrs = noAttrs
44
45 def initialize(self):
46 """
47 Public method to initialize the object.
48 """
49 pname = self.project.getProjectName()
50 if pname and self.project.isProjectSource(self.file):
51 name = self.tr("Class Diagram {0}: {1}").format(
52 pname, self.project.getRelativePath(self.file))
53 else:
54 name = self.tr("Class Diagram: {0}").format(self.file)
55 self.umlView.setDiagramName(name)
56
57 def __getCurrentShape(self, name):
58 """
59 Private method to get the named shape.
60
61 @param name name of the shape (string)
62 @return shape (QGraphicsItem)
63 """
64 return self.allClasses.get(name)
65
66 def buildDiagram(self):
67 """
68 Public method to build the class shapes of the class diagram.
69
70 The algorithm is borrowed from Boa Constructor.
71 """
72 import Utilities.ModuleParser
73
74 self.allClasses = {}
75 self.allModules = {}
76
77 try:
78 extensions = Preferences.getPython("PythonExtensions") + \
79 Preferences.getPython("Python3Extensions") + ['.rb']
80 module = Utilities.ModuleParser.readModule(
81 self.file, extensions=extensions, caching=False)
82 except ImportError:
83 ct = QGraphicsTextItem(None)
84 ct.setHtml(
85 self.tr("The module <b>'{0}'</b> could not be found.")
86 .format(self.file))
87 self.scene.addItem(ct)
88 return
89
90 if self.file not in self.allModules:
91 self.allModules[self.file] = []
92
93 routes = []
94 nodes = []
95 todo = [module.createHierarchy()]
96 classesFound = False
97 while todo:
98 hierarchy = todo[0]
99 for className in hierarchy:
100 classesFound = True
101 cw = self.__getCurrentShape(className)
102 if not cw and className.find('.') >= 0:
103 cw = self.__getCurrentShape(className.split('.')[-1])
104 if cw:
105 self.allClasses[className] = cw
106 if className not in self.allModules[self.file]:
107 self.allModules[self.file].append(className)
108 if cw and cw.noAttrs != self.noAttrs:
109 cw = None
110 if cw and not (cw.external and
111 (className in module.classes or
112 className in module.modules)):
113 if cw.scene() != self.scene:
114 self.scene.addItem(cw)
115 cw.setPos(10, 10)
116 if className not in nodes:
117 nodes.append(className)
118 else:
119 if className in module.classes:
120 # this is a local class (defined in this module)
121 self.__addLocalClass(
122 className, module.classes[className], 0, 0)
123 elif className in module.modules:
124 # this is a local module (defined in this module)
125 self.__addLocalClass(
126 className, module.modules[className], 0, 0, True)
127 else:
128 self.__addExternalClass(className, 0, 0)
129 nodes.append(className)
130
131 if hierarchy.get(className):
132 todo.append(hierarchy.get(className))
133 children = list(hierarchy.get(className).keys())
134 for child in children:
135 if (className, child) not in routes:
136 routes.append((className, child))
137
138 del todo[0]
139
140 if classesFound:
141 self.__arrangeClasses(nodes, routes[:])
142 self.__createAssociations(routes)
143 self.umlView.autoAdjustSceneSize(limit=True)
144 else:
145 ct = QGraphicsTextItem(None)
146 ct.setHtml(self.tr(
147 "The module <b>'{0}'</b> does not contain any classes.")
148 .format(self.file))
149 self.scene.addItem(ct)
150
151 def __arrangeClasses(self, nodes, routes, whiteSpaceFactor=1.2):
152 """
153 Private method to arrange the shapes on the canvas.
154
155 The algorithm is borrowed from Boa Constructor.
156
157 @param nodes list of nodes to arrange
158 @param routes list of routes
159 @param whiteSpaceFactor factor to increase whitespace between
160 items (float)
161 """
162 from . import GraphicsUtilities
163 generations = GraphicsUtilities.sort(nodes, routes)
164
165 # calculate width and height of all elements
166 sizes = []
167 for generation in generations:
168 sizes.append([])
169 for child in generation:
170 sizes[-1].append(
171 self.__getCurrentShape(child).sceneBoundingRect())
172
173 # calculate total width and total height
174 width = 0
175 height = 0
176 widths = []
177 heights = []
178 for generation in sizes:
179 currentWidth = 0
180 currentHeight = 0
181
182 for rect in generation:
183 if rect.bottom() > currentHeight:
184 currentHeight = rect.bottom()
185 currentWidth = currentWidth + rect.right()
186
187 # update totals
188 if currentWidth > width:
189 width = currentWidth
190 height = height + currentHeight
191
192 # store generation info
193 widths.append(currentWidth)
194 heights.append(currentHeight)
195
196 # add in some whitespace
197 width = width * whiteSpaceFactor
198 height = height * whiteSpaceFactor - 20
199 verticalWhiteSpace = 40.0
200
201 sceneRect = self.umlView.sceneRect()
202 width += 50.0
203 height += 50.0
204 swidth = width < sceneRect.width() and sceneRect.width() or width
205 sheight = height < sceneRect.height() and sceneRect.height() or height
206 self.umlView.setSceneSize(swidth, sheight)
207
208 # distribute each generation across the width and the
209 # generations across height
210 y = 10.0
211 for currentWidth, currentHeight, generation in \
212 zip_longest(widths, heights, generations):
213 x = 10.0
214 # whiteSpace is the space between any two elements
215 whiteSpace = (width - currentWidth - 20) / \
216 (len(generation) - 1.0 or 2.0)
217 for className in generation:
218 cw = self.__getCurrentShape(className)
219 cw.setPos(x, y)
220 rect = cw.sceneBoundingRect()
221 x = x + rect.width() + whiteSpace
222 y = y + currentHeight + verticalWhiteSpace
223
224 def __addLocalClass(self, className, _class, x, y, isRbModule=False):
225 """
226 Private method to add a class defined in the module.
227
228 @param className name of the class to be as a dictionary key (string)
229 @param _class class to be shown (ModuleParser.Class)
230 @param x x-coordinate (float)
231 @param y y-coordinate (float)
232 @param isRbModule flag indicating a Ruby module (boolean)
233 """
234 from .ClassItem import ClassItem, ClassModel
235 meths = sorted(_class.methods.keys())
236 attrs = sorted(_class.attributes.keys())
237 name = _class.name
238 if isRbModule:
239 name = "{0} (Module)".format(name)
240 cl = ClassModel(name, meths[:], attrs[:])
241 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene)
242 cw.setId(self.umlView.getItemId())
243 self.allClasses[className] = cw
244 if _class.name not in self.allModules[self.file]:
245 self.allModules[self.file].append(_class.name)
246
247 def __addExternalClass(self, _class, x, y):
248 """
249 Private method to add a class defined outside the module.
250
251 If the canvas is too small to take the shape, it
252 is enlarged.
253
254 @param _class class to be shown (string)
255 @param x x-coordinate (float)
256 @param y y-coordinate (float)
257 """
258 from .ClassItem import ClassItem, ClassModel
259 cl = ClassModel(_class)
260 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene)
261 cw.setId(self.umlView.getItemId())
262 self.allClasses[_class] = cw
263 if _class not in self.allModules[self.file]:
264 self.allModules[self.file].append(_class)
265
266 def __createAssociations(self, routes):
267 """
268 Private method to generate the associations between the class shapes.
269
270 @param routes list of relationsships
271 """
272 from .AssociationItem import AssociationItem, Generalisation
273 for route in routes:
274 if len(route) > 1:
275 assoc = AssociationItem(
276 self.__getCurrentShape(route[1]),
277 self.__getCurrentShape(route[0]),
278 Generalisation,
279 topToBottom=True)
280 self.scene.addItem(assoc)
281
282 def getPersistenceData(self):
283 """
284 Public method to get a string for data to be persisted.
285
286 @return persisted data string (string)
287 """
288 return "file={0}, no_attributes={1}".format(self.file, self.noAttrs)
289
290 def parsePersistenceData(self, version, data):
291 """
292 Public method to parse persisted data.
293
294 @param version version of the data (string)
295 @param data persisted data to be parsed (string)
296 @return flag indicating success (boolean)
297 """
298 parts = data.split(", ")
299 if len(parts) != 2 or \
300 not parts[0].startswith("file=") or \
301 not parts[1].startswith("no_attributes="):
302 return False
303
304 self.file = parts[0].split("=", 1)[1].strip()
305 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip())
306
307 self.initialize()
308
309 return True

eric ide

mercurial