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

eric ide

mercurial