Graphics/UMLClassDiagram.py

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

eric ide

mercurial