src/eric7/Graphics/UMLClassDiagramBuilder.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9278
36448ca469c2
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
20 20
21 class UMLClassDiagramBuilder(UMLDiagramBuilder): 21 class UMLClassDiagramBuilder(UMLDiagramBuilder):
22 """ 22 """
23 Class implementing a builder for UML like class diagrams. 23 Class implementing a builder for UML like class diagrams.
24 """ 24 """
25
25 def __init__(self, dialog, view, project, file, noAttrs=False): 26 def __init__(self, dialog, view, project, file, noAttrs=False):
26 """ 27 """
27 Constructor 28 Constructor
28 29
29 @param dialog reference to the UML dialog 30 @param dialog reference to the UML dialog
30 @type UMLDialog 31 @type UMLDialog
31 @param view reference to the view object 32 @param view reference to the view object
32 @type UMLGraphicsView 33 @type UMLGraphicsView
33 @param project reference to the project object 34 @param project reference to the project object
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:
385 msg = self.tr( 407 msg = self.tr(
386 "<p>The diagram belongs to project <b>{0}</b>." 408 "<p>The diagram belongs to project <b>{0}</b>."
387 " Please open it and try again.</p>" 409 " Please open it and try again.</p>"
388 ).format(data["project_name"]) 410 ).format(data["project_name"])
389 return False, msg 411 return False, msg
390 412
391 self.__relFile = file 413 self.__relFile = file
392 self.file = self.project.getAbsolutePath(file) 414 self.file = self.project.getAbsolutePath(file)
393 except KeyError: 415 except KeyError:
394 return False, "" 416 return False, ""
395 417
396 self.initialize() 418 self.initialize()
397 419
398 return True, "" 420 return True, ""

eric ide

mercurial