37 @param noAttrs flag indicating, that no attributes should be shown |
38 @param noAttrs flag indicating, that no attributes should be shown |
38 @type bool |
39 @type bool |
39 """ |
40 """ |
40 super().__init__(dialog, view, project) |
41 super().__init__(dialog, view, project) |
41 self.setObjectName("UMLClassDiagramBuilder") |
42 self.setObjectName("UMLClassDiagramBuilder") |
42 |
43 |
43 self.file = file |
44 self.file = file |
44 self.noAttrs = noAttrs |
45 self.noAttrs = noAttrs |
45 |
46 |
46 self.__relFile = ( |
47 self.__relFile = ( |
47 self.project.getRelativePath(self.file) |
48 self.project.getRelativePath(self.file) |
48 if self.project.isProjectSource(self.file) else |
49 if self.project.isProjectSource(self.file) |
49 "" |
50 else "" |
50 ) |
51 ) |
51 |
52 |
52 def initialize(self): |
53 def initialize(self): |
53 """ |
54 """ |
54 Public method to initialize the object. |
55 Public method to initialize the object. |
55 """ |
56 """ |
56 pname = self.project.getProjectName() |
57 pname = self.project.getProjectName() |
57 name = ( |
58 name = ( |
58 self.tr("Class Diagram {0}: {1}").format( |
59 self.tr("Class Diagram {0}: {1}").format( |
59 pname, self.project.getRelativePath(self.file)) |
60 pname, self.project.getRelativePath(self.file) |
60 if pname and self.project.isProjectSource(self.file) else |
61 ) |
61 self.tr("Class Diagram: {0}").format(self.file) |
62 if pname and self.project.isProjectSource(self.file) |
|
63 else self.tr("Class Diagram: {0}").format(self.file) |
62 ) |
64 ) |
63 self.umlView.setDiagramName(name) |
65 self.umlView.setDiagramName(name) |
64 |
66 |
65 def __getCurrentShape(self, name): |
67 def __getCurrentShape(self, name): |
66 """ |
68 """ |
67 Private method to get the named shape. |
69 Private method to get the named shape. |
68 |
70 |
69 @param name name of the shape |
71 @param name name of the shape |
70 @type str |
72 @type str |
71 @return shape |
73 @return shape |
72 @rtype QGraphicsItem |
74 @rtype QGraphicsItem |
73 """ |
75 """ |
74 return self.allClasses.get(name) |
76 return self.allClasses.get(name) |
75 |
77 |
76 def buildDiagram(self): |
78 def buildDiagram(self): |
77 """ |
79 """ |
78 Public method to build the class shapes of the class diagram. |
80 Public method to build the class shapes of the class diagram. |
79 |
81 |
80 The algorithm is borrowed from Boa Constructor. |
82 The algorithm is borrowed from Boa Constructor. |
81 """ |
83 """ |
82 import Utilities.ModuleParser |
84 import Utilities.ModuleParser |
83 |
85 |
84 self.allClasses = {} |
86 self.allClasses = {} |
85 self.allModules = {} |
87 self.allModules = {} |
86 |
88 |
87 try: |
89 try: |
88 extensions = ( |
90 extensions = Preferences.getPython("Python3Extensions") + [".rb"] |
89 Preferences.getPython("Python3Extensions") + |
91 module = Utilities.ModuleParser.readModule( |
90 ['.rb'] |
92 self.file, extensions=extensions, caching=False |
91 ) |
93 ) |
92 module = Utilities.ModuleParser.readModule( |
|
93 self.file, extensions=extensions, caching=False) |
|
94 except ImportError: |
94 except ImportError: |
95 ct = QGraphicsTextItem(None) |
95 ct = QGraphicsTextItem(None) |
96 ct.setHtml(self.buildErrorMessage( |
96 ct.setHtml( |
97 self.tr("The module <b>'{0}'</b> could not be found.") |
97 self.buildErrorMessage( |
98 .format(self.file) |
98 self.tr("The module <b>'{0}'</b> could not be found.").format( |
99 )) |
99 self.file |
|
100 ) |
|
101 ) |
|
102 ) |
100 self.scene.addItem(ct) |
103 self.scene.addItem(ct) |
101 return |
104 return |
102 |
105 |
103 if self.file not in self.allModules: |
106 if self.file not in self.allModules: |
104 self.allModules[self.file] = [] |
107 self.allModules[self.file] = [] |
105 |
108 |
106 routes = [] |
109 routes = [] |
107 nodes = [] |
110 nodes = [] |
108 todo = [module.createHierarchy()] |
111 todo = [module.createHierarchy()] |
109 classesFound = False |
112 classesFound = False |
110 while todo: |
113 while todo: |
111 hierarchy = todo[0] |
114 hierarchy = todo[0] |
112 for className in hierarchy: |
115 for className in hierarchy: |
113 classesFound = True |
116 classesFound = True |
114 cw = self.__getCurrentShape(className) |
117 cw = self.__getCurrentShape(className) |
115 if not cw and className.find('.') >= 0: |
118 if not cw and className.find(".") >= 0: |
116 cw = self.__getCurrentShape(className.split('.')[-1]) |
119 cw = self.__getCurrentShape(className.split(".")[-1]) |
117 if cw: |
120 if cw: |
118 self.allClasses[className] = cw |
121 self.allClasses[className] = cw |
119 if className not in self.allModules[self.file]: |
122 if className not in self.allModules[self.file]: |
120 self.allModules[self.file].append(className) |
123 self.allModules[self.file].append(className) |
121 if cw and cw.noAttrs != self.noAttrs: |
124 if cw and cw.noAttrs != self.noAttrs: |
122 cw = None |
125 cw = None |
123 if cw and not (cw.external and |
126 if cw and not ( |
124 (className in module.classes or |
127 cw.external |
125 className in module.modules)): |
128 and (className in module.classes or className in module.modules) |
|
129 ): |
126 if cw.scene() != self.scene: |
130 if cw.scene() != self.scene: |
127 self.scene.addItem(cw) |
131 self.scene.addItem(cw) |
128 cw.setPos(10, 10) |
132 cw.setPos(10, 10) |
129 if className not in nodes: |
133 if className not in nodes: |
130 nodes.append(className) |
134 nodes.append(className) |
131 else: |
135 else: |
132 if className in module.classes: |
136 if className in module.classes: |
133 # this is a local class (defined in this module) |
137 # this is a local class (defined in this module) |
134 self.__addLocalClass( |
138 self.__addLocalClass(className, module.classes[className], 0, 0) |
135 className, module.classes[className], 0, 0) |
|
136 elif className in module.modules: |
139 elif className in module.modules: |
137 # this is a local module (defined in this module) |
140 # this is a local module (defined in this module) |
138 self.__addLocalClass( |
141 self.__addLocalClass( |
139 className, module.modules[className], 0, 0, True) |
142 className, module.modules[className], 0, 0, True |
|
143 ) |
140 else: |
144 else: |
141 self.__addExternalClass(className, 0, 0) |
145 self.__addExternalClass(className, 0, 0) |
142 nodes.append(className) |
146 nodes.append(className) |
143 |
147 |
144 if hierarchy.get(className): |
148 if hierarchy.get(className): |
145 todo.append(hierarchy.get(className)) |
149 todo.append(hierarchy.get(className)) |
146 children = list(hierarchy.get(className).keys()) |
150 children = list(hierarchy.get(className).keys()) |
147 for child in children: |
151 for child in children: |
148 if (className, child) not in routes: |
152 if (className, child) not in routes: |
149 routes.append((className, child)) |
153 routes.append((className, child)) |
150 |
154 |
151 del todo[0] |
155 del todo[0] |
152 |
156 |
153 if classesFound: |
157 if classesFound: |
154 self.__arrangeClasses(nodes, routes[:]) |
158 self.__arrangeClasses(nodes, routes[:]) |
155 self.__createAssociations(routes) |
159 self.__createAssociations(routes) |
156 self.umlView.autoAdjustSceneSize(limit=True) |
160 self.umlView.autoAdjustSceneSize(limit=True) |
157 else: |
161 else: |
158 ct = QGraphicsTextItem(None) |
162 ct = QGraphicsTextItem(None) |
159 ct.setHtml(self.buildErrorMessage( |
163 ct.setHtml( |
160 self.tr("The module <b>'{0}'</b> does not contain any" |
164 self.buildErrorMessage( |
161 " classes.").format(self.file) |
165 self.tr( |
162 )) |
166 "The module <b>'{0}'</b> does not contain any" " classes." |
|
167 ).format(self.file) |
|
168 ) |
|
169 ) |
163 self.scene.addItem(ct) |
170 self.scene.addItem(ct) |
164 |
171 |
165 def __arrangeClasses(self, nodes, routes, whiteSpaceFactor=1.2): |
172 def __arrangeClasses(self, nodes, routes, whiteSpaceFactor=1.2): |
166 """ |
173 """ |
167 Private method to arrange the shapes on the canvas. |
174 Private method to arrange the shapes on the canvas. |
168 |
175 |
169 The algorithm is borrowed from Boa Constructor. |
176 The algorithm is borrowed from Boa Constructor. |
170 |
177 |
171 @param nodes list of nodes to arrange |
178 @param nodes list of nodes to arrange |
172 @type list of str |
179 @type list of str |
173 @param routes list of routes |
180 @param routes list of routes |
174 @type list of tuple of (str, str) |
181 @type list of tuple of (str, str) |
175 @param whiteSpaceFactor factor to increase whitespace between |
182 @param whiteSpaceFactor factor to increase whitespace between |
176 items |
183 items |
177 @type float |
184 @type float |
178 """ |
185 """ |
179 from . import GraphicsUtilities |
186 from . import GraphicsUtilities |
|
187 |
180 generations = GraphicsUtilities.sort(nodes, routes) |
188 generations = GraphicsUtilities.sort(nodes, routes) |
181 |
189 |
182 # calculate width and height of all elements |
190 # calculate width and height of all elements |
183 sizes = [] |
191 sizes = [] |
184 for generation in generations: |
192 for generation in generations: |
185 sizes.append([]) |
193 sizes.append([]) |
186 for child in generation: |
194 for child in generation: |
187 sizes[-1].append( |
195 sizes[-1].append(self.__getCurrentShape(child).sceneBoundingRect()) |
188 self.__getCurrentShape(child).sceneBoundingRect()) |
196 |
189 |
|
190 # calculate total width and total height |
197 # calculate total width and total height |
191 width = 0 |
198 width = 0 |
192 height = 0 |
199 height = 0 |
193 widths = [] |
200 widths = [] |
194 heights = [] |
201 heights = [] |
195 for generation in sizes: |
202 for generation in sizes: |
196 currentWidth = 0 |
203 currentWidth = 0 |
197 currentHeight = 0 |
204 currentHeight = 0 |
198 |
205 |
199 for rect in generation: |
206 for rect in generation: |
200 if rect.bottom() > currentHeight: |
207 if rect.bottom() > currentHeight: |
201 currentHeight = rect.bottom() |
208 currentHeight = rect.bottom() |
202 currentWidth += rect.right() |
209 currentWidth += rect.right() |
203 |
210 |
204 # update totals |
211 # update totals |
205 if currentWidth > width: |
212 if currentWidth > width: |
206 width = currentWidth |
213 width = currentWidth |
207 height += currentHeight |
214 height += currentHeight |
208 |
215 |
209 # store generation info |
216 # store generation info |
210 widths.append(currentWidth) |
217 widths.append(currentWidth) |
211 heights.append(currentHeight) |
218 heights.append(currentHeight) |
212 |
219 |
213 # add in some whitespace |
220 # add in some whitespace |
214 width *= whiteSpaceFactor |
221 width *= whiteSpaceFactor |
215 height = height * whiteSpaceFactor - 20 |
222 height = height * whiteSpaceFactor - 20 |
216 verticalWhiteSpace = 40.0 |
223 verticalWhiteSpace = 40.0 |
217 |
224 |
218 sceneRect = self.umlView.sceneRect() |
225 sceneRect = self.umlView.sceneRect() |
219 width += 50.0 |
226 width += 50.0 |
220 height += 50.0 |
227 height += 50.0 |
221 swidth = sceneRect.width() if width < sceneRect.width() else width |
228 swidth = sceneRect.width() if width < sceneRect.width() else width |
222 sheight = sceneRect.height() if height < sceneRect.height() else height |
229 sheight = sceneRect.height() if height < sceneRect.height() else height |
223 self.umlView.setSceneSize(swidth, sheight) |
230 self.umlView.setSceneSize(swidth, sheight) |
224 |
231 |
225 # distribute each generation across the width and the |
232 # distribute each generation across the width and the |
226 # generations across height |
233 # generations across height |
227 y = 10.0 |
234 y = 10.0 |
228 for currentWidth, currentHeight, generation in ( |
235 for currentWidth, currentHeight, generation in zip_longest( |
229 zip_longest(widths, heights, generations) |
236 widths, heights, generations |
230 ): |
237 ): |
231 x = 10.0 |
238 x = 10.0 |
232 # whiteSpace is the space between any two elements |
239 # whiteSpace is the space between any two elements |
233 whiteSpace = ( |
240 whiteSpace = (width - currentWidth - 20) / (len(generation) - 1.0 or 2.0) |
234 (width - currentWidth - 20) / |
|
235 (len(generation) - 1.0 or 2.0) |
|
236 ) |
|
237 for className in generation: |
241 for className in generation: |
238 cw = self.__getCurrentShape(className) |
242 cw = self.__getCurrentShape(className) |
239 cw.setPos(x, y) |
243 cw.setPos(x, y) |
240 rect = cw.sceneBoundingRect() |
244 rect = cw.sceneBoundingRect() |
241 x = x + rect.width() + whiteSpace |
245 x = x + rect.width() + whiteSpace |
242 y = y + currentHeight + verticalWhiteSpace |
246 y = y + currentHeight + verticalWhiteSpace |
243 |
247 |
244 def __addLocalClass(self, className, _class, x, y, isRbModule=False): |
248 def __addLocalClass(self, className, _class, x, y, isRbModule=False): |
245 """ |
249 """ |
246 Private method to add a class defined in the module. |
250 Private method to add a class defined in the module. |
247 |
251 |
248 @param className name of the class to be as a dictionary key |
252 @param className name of the class to be as a dictionary key |
249 @type str |
253 @type str |
250 @param _class class to be shown |
254 @param _class class to be shown |
251 @type ModuleParser.Class |
255 @type ModuleParser.Class |
252 @param x x-coordinate |
256 @param x x-coordinate |
255 @type float |
259 @type float |
256 @param isRbModule flag indicating a Ruby module |
260 @param isRbModule flag indicating a Ruby module |
257 @type bool |
261 @type bool |
258 """ |
262 """ |
259 from .ClassItem import ClassItem, ClassModel |
263 from .ClassItem import ClassItem, ClassModel |
|
264 |
260 name = _class.name |
265 name = _class.name |
261 if isRbModule: |
266 if isRbModule: |
262 name = "{0} (Module)".format(name) |
267 name = "{0} (Module)".format(name) |
263 cl = ClassModel( |
268 cl = ClassModel( |
264 name, |
269 name, |
265 sorted(_class.methods.keys())[:], |
270 sorted(_class.methods.keys())[:], |
266 sorted(_class.attributes.keys())[:], |
271 sorted(_class.attributes.keys())[:], |
267 sorted(_class.globals.keys())[:] |
272 sorted(_class.globals.keys())[:], |
268 ) |
273 ) |
269 cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene, |
274 cw = ClassItem( |
270 colors=self.umlView.getDrawingColors()) |
275 cl, |
|
276 False, |
|
277 x, |
|
278 y, |
|
279 noAttrs=self.noAttrs, |
|
280 scene=self.scene, |
|
281 colors=self.umlView.getDrawingColors(), |
|
282 ) |
271 cw.setId(self.umlView.getItemId()) |
283 cw.setId(self.umlView.getItemId()) |
272 self.allClasses[className] = cw |
284 self.allClasses[className] = cw |
273 if _class.name not in self.allModules[self.file]: |
285 if _class.name not in self.allModules[self.file]: |
274 self.allModules[self.file].append(_class.name) |
286 self.allModules[self.file].append(_class.name) |
275 |
287 |
276 def __addExternalClass(self, _class, x, y): |
288 def __addExternalClass(self, _class, x, y): |
277 """ |
289 """ |
278 Private method to add a class defined outside the module. |
290 Private method to add a class defined outside the module. |
279 |
291 |
280 If the canvas is too small to take the shape, it |
292 If the canvas is too small to take the shape, it |
281 is enlarged. |
293 is enlarged. |
282 |
294 |
283 @param _class class to be shown |
295 @param _class class to be shown |
284 @type ModuleParser.Class |
296 @type ModuleParser.Class |
285 @param x x-coordinate |
297 @param x x-coordinate |
286 @type float |
298 @type float |
287 @param y y-coordinate |
299 @param y y-coordinate |
288 @type float |
300 @type float |
289 """ |
301 """ |
290 from .ClassItem import ClassItem, ClassModel |
302 from .ClassItem import ClassItem, ClassModel |
|
303 |
291 cl = ClassModel(_class) |
304 cl = ClassModel(_class) |
292 cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene, |
305 cw = ClassItem( |
293 colors=self.umlView.getDrawingColors()) |
306 cl, |
|
307 True, |
|
308 x, |
|
309 y, |
|
310 noAttrs=self.noAttrs, |
|
311 scene=self.scene, |
|
312 colors=self.umlView.getDrawingColors(), |
|
313 ) |
294 cw.setId(self.umlView.getItemId()) |
314 cw.setId(self.umlView.getItemId()) |
295 self.allClasses[_class] = cw |
315 self.allClasses[_class] = cw |
296 if _class not in self.allModules[self.file]: |
316 if _class not in self.allModules[self.file]: |
297 self.allModules[self.file].append(_class) |
317 self.allModules[self.file].append(_class) |
298 |
318 |
299 def __createAssociations(self, routes): |
319 def __createAssociations(self, routes): |
300 """ |
320 """ |
301 Private method to generate the associations between the class shapes. |
321 Private method to generate the associations between the class shapes. |
302 |
322 |
303 @param routes list of relationsships |
323 @param routes list of relationsships |
304 @type list of tuple of (str, str) |
324 @type list of tuple of (str, str) |
305 """ |
325 """ |
306 from .AssociationItem import AssociationItem, AssociationType |
326 from .AssociationItem import AssociationItem, AssociationType |
|
327 |
307 for route in routes: |
328 for route in routes: |
308 if len(route) > 1: |
329 if len(route) > 1: |
309 assoc = AssociationItem( |
330 assoc = AssociationItem( |
310 self.__getCurrentShape(route[1]), |
331 self.__getCurrentShape(route[1]), |
311 self.__getCurrentShape(route[0]), |
332 self.__getCurrentShape(route[0]), |
312 AssociationType.GENERALISATION, |
333 AssociationType.GENERALISATION, |
313 topToBottom=True, |
334 topToBottom=True, |
314 colors=self.umlView.getDrawingColors()) |
335 colors=self.umlView.getDrawingColors(), |
|
336 ) |
315 self.scene.addItem(assoc) |
337 self.scene.addItem(assoc) |
316 |
338 |
317 def parsePersistenceData(self, version, data): |
339 def parsePersistenceData(self, version, data): |
318 """ |
340 """ |
319 Public method to parse persisted data. |
341 Public method to parse persisted data. |
320 |
342 |
321 @param version version of the data |
343 @param version version of the data |
322 @type str |
344 @type str |
323 @param data persisted data to be parsed |
345 @param data persisted data to be parsed |
324 @type str |
346 @type str |
325 @return flag indicating success |
347 @return flag indicating success |
326 @rtype bool |
348 @rtype bool |
327 """ |
349 """ |
328 parts = data.split(", ") |
350 parts = data.split(", ") |
329 if ( |
351 if ( |
330 len(parts) != 2 or |
352 len(parts) != 2 |
331 not parts[0].startswith("file=") or |
353 or not parts[0].startswith("file=") |
332 not parts[1].startswith("no_attributes=") |
354 or not parts[1].startswith("no_attributes=") |
333 ): |
355 ): |
334 return False |
356 return False |
335 |
357 |
336 self.file = parts[0].split("=", 1)[1].strip() |
358 self.file = parts[0].split("=", 1)[1].strip() |
337 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip()) |
359 self.noAttrs = Utilities.toBool(parts[1].split("=", 1)[1].strip()) |
338 |
360 |
339 self.initialize() |
361 self.initialize() |
340 |
362 |
341 return True |
363 return True |
342 |
364 |
343 def toDict(self): |
365 def toDict(self): |
344 """ |
366 """ |
345 Public method to collect data to be persisted. |
367 Public method to collect data to be persisted. |
346 |
368 |
347 @return dictionary containing data to be persisted |
369 @return dictionary containing data to be persisted |
348 @rtype dict |
370 @rtype dict |
349 """ |
371 """ |
350 data = { |
372 data = { |
351 "project_name": self.project.getProjectName(), |
373 "project_name": self.project.getProjectName(), |
352 "no_attributes": self.noAttrs, |
374 "no_attributes": self.noAttrs, |
353 } |
375 } |
354 |
376 |
355 data["file"] = ( |
377 data["file"] = ( |
356 Utilities.fromNativeSeparators(self.__relFile) |
378 Utilities.fromNativeSeparators(self.__relFile) |
357 if self.__relFile else |
379 if self.__relFile |
358 Utilities.fromNativeSeparators(self.file) |
380 else Utilities.fromNativeSeparators(self.file) |
359 ) |
381 ) |
360 |
382 |
361 return data |
383 return data |
362 |
384 |
363 def fromDict(self, version, data): |
385 def fromDict(self, version, data): |
364 """ |
386 """ |
365 Public method to populate the class with data persisted by 'toDict()'. |
387 Public method to populate the class with data persisted by 'toDict()'. |
366 |
388 |
367 @param version version of the data |
389 @param version version of the data |
368 @type str |
390 @type str |
369 @param data dictionary containing the persisted data |
391 @param data dictionary containing the persisted data |
370 @type dict |
392 @type dict |
371 @return tuple containing a flag indicating success and an info |
393 @return tuple containing a flag indicating success and an info |
372 message in case the diagram belongs to a different project |
394 message in case the diagram belongs to a different project |
373 @rtype tuple of (bool, str) |
395 @rtype tuple of (bool, str) |
374 """ |
396 """ |
375 try: |
397 try: |
376 self.noAttrs = data["no_attributes"] |
398 self.noAttrs = data["no_attributes"] |
377 |
399 |
378 file = Utilities.toNativeSeparators(data["file"]) |
400 file = Utilities.toNativeSeparators(data["file"]) |
379 if os.path.isabs(file): |
401 if os.path.isabs(file): |
380 self.file = file |
402 self.file = file |
381 self.__relFile = "" |
403 self.__relFile = "" |
382 else: |
404 else: |