Graphics/PackageDiagram.py

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

eric ide

mercurial