Graphics/PackageDiagram.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 of a package.
8 """
9
10 import glob
11 import os.path
12
13 from PyQt4.QtCore import *
14 from PyQt4.QtGui import *
15
16 from UMLDialog import UMLDialog
17 from ClassItem import ClassItem, ClassModel
18 from AssociationItem import AssociationItem, Generalisation
19 import GraphicsUtilities
20
21 import Utilities.ModuleParser
22 import Utilities
23
24 class PackageDiagram(UMLDialog):
25 """
26 Class implementing a dialog showing a UML like class diagram of a package.
27 """
28 def __init__(self, package, parent = None, name = None, noAttrs = False):
29 """
30 Constructor
31
32 @param package name of a python package to be shown (string)
33 @param parent parent widget of the view (QWidget)
34 @param name name of the view widget (string)
35 @keyparam noAttrs flag indicating, that no attributes should be shown (boolean)
36 """
37 self.package = Utilities.normabspath(package)
38 self.allClasses = {}
39 self.noAttrs = noAttrs
40
41 UMLDialog.__init__(self, self.package, parent)
42
43 if not name:
44 self.setObjectName("PackageDiagram")
45 else:
46 self.setObjectName(name)
47
48 self.connect(self.umlView, SIGNAL("relayout()"), self.relayout)
49
50 def __getCurrentShape(self, name):
51 """
52 Private method to get the named shape.
53
54 @param name name of the shape (string)
55 @return shape (QCanvasItem)
56 """
57 return self.allClasses.get(name)
58
59 def __buildModulesDict(self):
60 """
61 Private method to build a dictionary of modules contained in the package.
62
63 @return dictionary of modules contained in the package.
64 """
65 # TODO: change this to use configured extensions
66 supportedExt = ['*.py', '*.pyw', '*.ptl', '*.rb']
67
68 moduleDict = {}
69 modules = []
70 for ext in supportedExt:
71 modules.extend(glob.glob(Utilities.normjoinpath(self.package, ext)))
72 tot = len(modules)
73 try:
74 prog = 0
75 progress = QProgressDialog(self.trUtf8("Parsing modules..."),
76 "", 0, tot, self)
77 progress.show()
78 QApplication.processEvents()
79 for module in modules:
80 progress.setValue(prog)
81 QApplication.processEvents()
82 prog += 1
83 try:
84 mod = Utilities.ModuleParser.readModule(module)
85 except ImportError:
86 continue
87 else:
88 name = mod.name
89 if name.startswith(self.package):
90 name = name[len(self.package) + 1:]
91 moduleDict[name] = mod
92 finally:
93 progress.setValue(tot)
94 return moduleDict
95
96 def __buildClasses(self):
97 """
98 Private method to build the class shapes of the package diagram.
99
100 The algorithm is borrowed from Boa Constructor.
101 """
102 initlist = glob.glob(os.path.join(self.package, '__init__.*'))
103 if len(initlist) == 0:
104 ct = QGraphicsTextItem(None, self.scene)
105 ct.setHtml(\
106 self.trUtf8("The directory <b>'{0}'</b> is not a package.")\
107 .format(self.package))
108 return
109
110 modules = self.__buildModulesDict()
111 if not modules:
112 ct = QGraphicsTextItem(None, self.scene)
113 ct.setHtml(\
114 self.trUtf8("The package <b>'{0}'</b> does not contain any modules.")
115 .format(self.package))
116 return
117
118 # step 1: build all classes found in the modules
119 classesFound = False
120
121 for modName in modules.keys():
122 module = modules[modName]
123 for cls in module.classes.keys():
124 classesFound = True
125 self.__addLocalClass(cls, module.classes[cls], 0, 0)
126 if not classesFound:
127 ct = QGraphicsTextItem(None, self.scene)
128 ct.setHtml(\
129 self.trUtf8("The package <b>'{0}'</b> does not contain any classes.")
130 .format(self.package))
131 return
132
133 # step 2: build the class hierarchies
134 routes = []
135 nodes = []
136
137 for modName in modules.keys():
138 module = modules[modName]
139 todo = [module.createHierarchy()]
140 while todo:
141 hierarchy = todo[0]
142 for className in hierarchy.keys():
143 cw = self.__getCurrentShape(className)
144 if not cw and className.find('.') >= 0:
145 cw = self.__getCurrentShape(className.split('.')[-1])
146 if cw:
147 self.allClasses[className] = cw
148 if cw and cw.noAttrs != self.noAttrs:
149 cw = None
150 if cw and not (cw.external and \
151 (className in module.classes or
152 className in module.modules)
153 ):
154 if className not in nodes:
155 nodes.append(className)
156 else:
157 if className in module.classes:
158 # this is a local class (defined in this module)
159 self.__addLocalClass(className, module.classes[className],
160 0, 0)
161 elif className in module.modules:
162 # this is a local module (defined in this module)
163 self.__addLocalClass(className, module.modules[className],
164 0, 0, True)
165 else:
166 self.__addExternalClass(className, 0, 0)
167 nodes.append(className)
168
169 if hierarchy.get(className):
170 todo.append(hierarchy.get(className))
171 children = hierarchy.get(className).keys()
172 for child in children:
173 if (className, child) not in routes:
174 routes.append((className, child))
175
176 del todo[0]
177
178 self.__arrangeClasses(nodes, routes[:])
179 self.__createAssociations(routes)
180
181 def __arrangeClasses(self, nodes, routes, whiteSpaceFactor = 1.2):
182 """
183 Private method to arrange the shapes on the canvas.
184
185 The algorithm is borrowed from Boa Constructor.
186 """
187 generations = GraphicsUtilities.sort(nodes, routes)
188
189 # calculate width and height of all elements
190 sizes = []
191 for generation in generations:
192 sizes.append([])
193 for child in generation:
194 sizes[-1].append(self.__getCurrentShape(child).sceneBoundingRect())
195
196 # calculate total width and total height
197 width = 0
198 height = 0
199 widths = []
200 heights = []
201 for generation in sizes:
202 currentWidth = 0
203 currentHeight = 0
204
205 for rect in generation:
206 if rect.bottom() > currentHeight:
207 currentHeight = rect.bottom()
208 currentWidth = currentWidth + rect.right()
209
210 # update totals
211 if currentWidth > width:
212 width = currentWidth
213 height = height + currentHeight
214
215 # store generation info
216 widths.append(currentWidth)
217 heights.append(currentHeight)
218
219 # add in some whitespace
220 width = width * whiteSpaceFactor
221 rawHeight = height
222 height = height * whiteSpaceFactor - 20
223 verticalWhiteSpace = (height - rawHeight) / (len(generations) - 1.0 or 2.0)
224
225 sceneRect = self.umlView.sceneRect()
226 width += 50.0
227 height += 50.0
228 swidth = width < sceneRect.width() and sceneRect.width() or width
229 sheight = height < sceneRect.height() and sceneRect.height() or height
230 self.umlView.setSceneSize(swidth, sheight)
231
232 # distribute each generation across the width and the
233 # generations across height
234 y = 10.0
235 for currentWidth, currentHeight, generation in \
236 map(None, widths, heights, generations):
237 x = 10.0
238 # whiteSpace is the space between any two elements
239 whiteSpace = (width - currentWidth - 20) / (len(generation) - 1.0 or 2.0)
240 for className in generation:
241 cw = self.__getCurrentShape(className)
242 cw.setPos(x, y)
243 rect = cw.sceneBoundingRect()
244 x = x + rect.width() + whiteSpace
245 y = y + currentHeight + verticalWhiteSpace
246
247 def __addLocalClass(self, className, _class, x, y, isRbModule = False):
248 """
249 Private method to add a class defined in the module.
250
251 @param className name of the class to be as a dictionary key (string)
252 @param _class class to be shown (ModuleParser.Class)
253 @param x x-coordinate (float)
254 @param y y-coordinate (float)
255 @param isRbModule flag indicating a Ruby module (boolean)
256 """
257 meths = sorted(_class.methods.keys())
258 attrs = sorted(_class.attributes.keys())
259 name = _class.name
260 if isRbModule:
261 name = "%s (Module)" % name
262 cl = ClassModel(name, meths[:], attrs[:])
263 cw = ClassItem(cl, False, x, y, noAttrs = self.noAttrs, scene = self.scene)
264 self.allClasses[className] = cw
265
266 def __addExternalClass(self, _class, x, y):
267 """
268 Private method to add a class defined outside the module.
269
270 If the canvas is too small to take the shape, it
271 is enlarged.
272
273 @param _class class to be shown (string)
274 @param x x-coordinate (float)
275 @param y y-coordinate (float)
276 """
277 cl = ClassModel(_class)
278 cw = ClassItem(cl, True, x, y, noAttrs = self.noAttrs, scene = self.scene)
279 self.allClasses[_class] = cw
280
281 def __createAssociations(self, routes):
282 """
283 Private method to generate the associations between the class shapes.
284
285 @param routes list of relationsships
286 """
287 for route in routes:
288 if len(route) > 1:
289 assoc = AssociationItem(\
290 self.__getCurrentShape(route[1]),
291 self.__getCurrentShape(route[0]),
292 Generalisation)
293 self.scene.addItem(assoc)
294
295 def show(self):
296 """
297 Overriden method to show the dialog.
298 """
299 self.__buildClasses()
300 UMLDialog.show(self)
301
302 def relayout(self):
303 """
304 Method to relayout the diagram.
305 """
306 self.allClasses.clear()
307 self.__buildClasses()

eric ide

mercurial