Graphics/UMLClassDiagramBuilder.py

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

eric ide

mercurial