eric6/Graphics/ImportsDiagramBuilder.py

branch
maintenance
changeset 8400
b3eefd7e58d1
parent 8273
698ae46f40a4
parent 8295
3f5e8b0a338e
equal deleted inserted replaced
8274:197414ba11cc 8400:b3eefd7e58d1
30 def __init__(self, dialog, view, project, package, 30 def __init__(self, dialog, view, project, package,
31 showExternalImports=False): 31 showExternalImports=False):
32 """ 32 """
33 Constructor 33 Constructor
34 34
35 @param dialog reference to the UML dialog (UMLDialog) 35 @param dialog reference to the UML dialog
36 @param view reference to the view object (UMLGraphicsView) 36 @type UMLDialog
37 @param project reference to the project object (Project) 37 @param view reference to the view object
38 @type UMLGraphicsView
39 @param project reference to the project object
40 @type Project
38 @param package name of a python package to show the import 41 @param package name of a python package to show the import
39 relationships (string) 42 relationships
43 @type str
40 @param showExternalImports flag indicating to show exports from 44 @param showExternalImports flag indicating to show exports from
41 outside the package (boolean) 45 outside the package
46 @type bool
42 """ 47 """
43 super().__init__(dialog, view, project) 48 super().__init__(dialog, view, project)
44 self.setObjectName("ImportsDiagram") 49 self.setObjectName("ImportsDiagram")
45 50
46 self.showExternalImports = showExternalImports 51 self.showExternalImports = showExternalImports
47 self.packagePath = os.path.abspath(package) 52 self.packagePath = os.path.abspath(package)
53
54 self.__relPackagePath = (
55 self.project.getRelativePath(self.packagePath)
56 if self.project.isProjectSource(self.packagePath) else
57 ""
58 )
48 59
49 def initialize(self): 60 def initialize(self):
50 """ 61 """
51 Public method to initialize the object. 62 Public method to initialize the object.
52 """ 63 """
72 def __buildModulesDict(self): 83 def __buildModulesDict(self):
73 """ 84 """
74 Private method to build a dictionary of modules contained in the 85 Private method to build a dictionary of modules contained in the
75 package. 86 package.
76 87
77 @return dictionary of modules contained in the package. 88 @return dictionary of modules contained in the package
89 @rtype dict
78 """ 90 """
79 import Utilities.ModuleParser 91 import Utilities.ModuleParser
80 extensions = ( 92 extensions = (
81 Preferences.getPython("Python3Extensions") 93 Preferences.getPython("Python3Extensions")
82 ) 94 )
119 Public method to build the modules shapes of the diagram. 131 Public method to build the modules shapes of the diagram.
120 """ 132 """
121 initlist = glob.glob(os.path.join(self.packagePath, '__init__.*')) 133 initlist = glob.glob(os.path.join(self.packagePath, '__init__.*'))
122 if len(initlist) == 0: 134 if len(initlist) == 0:
123 ct = QGraphicsTextItem(None) 135 ct = QGraphicsTextItem(None)
124 ct.setHtml( 136 ct.setHtml(self.buildErrorMessage(
125 self.tr( 137 self.tr("The directory <b>'{0}'</b> is not a Python"
126 "The directory <b>'{0}'</b> is not a Python package.") 138 " package.").format(self.package)
127 .format(self.package)) 139 ))
128 self.scene.addItem(ct) 140 self.scene.addItem(ct)
129 return 141 return
130 142
131 shapes = {} 143 self.__shapes = {}
132 p = 10
133 y = 10
134 maxHeight = 0
135 sceneRect = self.umlView.sceneRect()
136 144
137 modules = self.__buildModulesDict() 145 modules = self.__buildModulesDict()
138 sortedkeys = sorted(modules.keys())
139 externalMods = [] 146 externalMods = []
140 packageList = self.shortPackage.split('.') 147 packageList = self.shortPackage.split('.')
141 packageListLen = len(packageList) 148 packageListLen = len(packageList)
142 for module in sortedkeys: 149 for module in sorted(modules.keys()):
143 impLst = [] 150 impLst = []
144 for i in modules[module].imports: 151 for importName in modules[module].imports:
145 n = (i[len(self.package) + 1:] 152 n = (
146 if i.startswith(self.package) else i) 153 importName[len(self.package) + 1:]
147 if i in modules: 154 if importName.startswith(self.package) else
155 importName
156 )
157 if importName in modules:
148 impLst.append(n) 158 impLst.append(n)
149 elif self.showExternalImports: 159 elif self.showExternalImports:
150 impLst.append(n) 160 impLst.append(n)
151 if n not in externalMods: 161 if n not in externalMods:
152 externalMods.append(n) 162 externalMods.append(n)
153 for i in list(modules[module].from_imports.keys()): 163 for importName in list(modules[module].from_imports.keys()):
154 if i.startswith('.'): 164 if importName.startswith('.'):
155 dots = len(i) - len(i.lstrip('.')) 165 dots = len(importName) - len(importName.lstrip('.'))
156 if dots == 1: 166 if dots == 1:
157 n = i[1:] 167 n = importName[1:]
158 i = n 168 importName = n
159 else: 169 else:
160 if self.showExternalImports: 170 if self.showExternalImports:
161 n = '.'.join( 171 n = '.'.join(
162 packageList[:packageListLen - dots + 1] + 172 packageList[:packageListLen - dots + 1] +
163 [i[dots:]]) 173 [importName[dots:]])
164 else: 174 else:
165 n = i 175 n = importName
166 elif i.startswith(self.package): 176 elif importName.startswith(self.package):
167 n = i[len(self.package) + 1:] 177 n = importName[len(self.package) + 1:]
168 else: 178 else:
169 n = i 179 n = importName
170 if i in modules: 180 if importName in modules:
171 impLst.append(n) 181 impLst.append(n)
172 elif self.showExternalImports: 182 elif self.showExternalImports:
173 impLst.append(n) 183 impLst.append(n)
174 if n not in externalMods: 184 if n not in externalMods:
175 externalMods.append(n) 185 externalMods.append(n)
186
176 classNames = [] 187 classNames = []
177 for cls in list(modules[module].classes.keys()): 188 for class_ in list(modules[module].classes.keys()):
178 className = modules[module].classes[cls].name 189 className = modules[module].classes[class_].name
179 if className not in classNames: 190 if className not in classNames:
180 classNames.append(className) 191 classNames.append(className)
181 shape = self.__addModule(module, classNames, 0.0, 0.0) 192 shape = self.__addModule(module, classNames, 0.0, 0.0)
182 shapeRect = shape.sceneBoundingRect() 193 self.__shapes[module] = (shape, impLst)
183 shapes[module] = (shape, impLst)
184 pn = p + shapeRect.width() + 10
185 maxHeight = max(maxHeight, shapeRect.height())
186 if pn > sceneRect.width():
187 p = 10
188 y += maxHeight + 10
189 maxHeight = shapeRect.height()
190 shape.setPos(p, y)
191 p += shapeRect.width() + 10
192 else:
193 shape.setPos(p, y)
194 p = pn
195 194
196 for module in externalMods: 195 for module in externalMods:
197 shape = self.__addModule(module, [], 0.0, 0.0) 196 shape = self.__addModule(module, [], 0.0, 0.0)
198 shapeRect = shape.sceneBoundingRect() 197 self.__shapes[module] = (shape, [])
199 shapes[module] = (shape, []) 198
200 pn = p + shapeRect.width() + 10 199 # build a list of routes
201 maxHeight = max(maxHeight, shapeRect.height()) 200 nodes = []
202 if pn > sceneRect.width(): 201 routes = []
203 p = 10 202 for module in self.__shapes:
204 y += maxHeight + 10 203 nodes.append(module)
205 maxHeight = shapeRect.height() 204 for rel in self.__shapes[module][1]:
206 shape.setPos(p, y) 205 route = (module, rel)
207 p += shapeRect.width() + 10 206 if route not in routes:
208 else: 207 routes.append(route)
209 shape.setPos(p, y) 208
210 p = pn 209 self.__arrangeNodes(nodes, routes[:])
211 210 self.__createAssociations(routes)
212 rect = self.umlView._getDiagramRect(10)
213 sceneRect = self.umlView.sceneRect()
214 if rect.width() > sceneRect.width():
215 sceneRect.setWidth(rect.width())
216 if rect.height() > sceneRect.height():
217 sceneRect.setHeight(rect.height())
218 self.umlView.setSceneSize(sceneRect.width(), sceneRect.height())
219
220 self.__createAssociations(shapes)
221 self.umlView.autoAdjustSceneSize(limit=True) 211 self.umlView.autoAdjustSceneSize(limit=True)
222 212
223 def __addModule(self, name, classes, x, y): 213 def __addModule(self, name, classes, x, y):
224 """ 214 """
225 Private method to add a module to the diagram. 215 Private method to add a module to the diagram.
226 216
227 @param name module name to be shown (string) 217 @param name module name to be shown
218 @type str
228 @param classes list of class names contained in the module 219 @param classes list of class names contained in the module
229 (list of strings) 220 @type list of str
230 @param x x-coordinate (float) 221 @param x x-coordinate
231 @param y y-coordinate (float) 222 @type float
232 @return reference to the imports item (ModuleItem) 223 @param y y-coordinate
224 @type float
225 @return reference to the imports item
226 @rtype ModuleItem
233 """ 227 """
234 from .ModuleItem import ModuleItem, ModuleModel 228 from .ModuleItem import ModuleItem, ModuleModel
235 classes.sort() 229 classes.sort()
236 impM = ModuleModel(name, classes) 230 impM = ModuleModel(name, classes)
237 impW = ModuleItem(impM, x, y, scene=self.scene, 231 impW = ModuleItem(impM, x, y, scene=self.scene,
238 colors=self.umlView.getDrawingColors()) 232 colors=self.umlView.getDrawingColors())
239 impW.setId(self.umlView.getItemId()) 233 impW.setId(self.umlView.getItemId())
240 return impW 234 return impW
241 235
242 def __createAssociations(self, shapes): 236 def __arrangeNodes(self, nodes, routes, whiteSpaceFactor=1.2):
237 """
238 Private method to arrange the shapes on the canvas.
239
240 The algorithm is borrowed from Boa Constructor.
241
242 @param nodes list of nodes to arrange
243 @type list of str
244 @param routes list of routes
245 @type list of tuple of (str, str)
246 @param whiteSpaceFactor factor to increase whitespace between
247 items
248 @type float
249 """
250 from . import GraphicsUtilities
251 generations = GraphicsUtilities.sort(nodes, routes)
252
253 # calculate width and height of all elements
254 sizes = []
255 for generation in generations:
256 sizes.append([])
257 for child in generation:
258 sizes[-1].append(
259 self.__shapes[child][0].sceneBoundingRect())
260
261 # calculate total width and total height
262 width = 0
263 height = 0
264 widths = []
265 heights = []
266 for generation in sizes:
267 currentWidth = 0
268 currentHeight = 0
269
270 for rect in generation:
271 if rect.height() > currentHeight:
272 currentHeight = rect.height()
273 currentWidth += rect.width()
274
275 # update totals
276 if currentWidth > width:
277 width = currentWidth
278 height += currentHeight
279
280 # store generation info
281 widths.append(currentWidth)
282 heights.append(currentHeight)
283
284 # add in some whitespace
285 width *= whiteSpaceFactor
286 height = height * whiteSpaceFactor - 20
287 verticalWhiteSpace = 40.0
288
289 sceneRect = self.umlView.sceneRect()
290 width += 50.0
291 height += 50.0
292 swidth = sceneRect.width() if width < sceneRect.width() else width
293 sheight = sceneRect.height() if height < sceneRect.height() else height
294 self.umlView.setSceneSize(swidth, sheight)
295
296 # distribute each generation across the width and the
297 # generations across height
298 y = 10.0
299 for currentWidth, currentHeight, generation in (
300 zip(reversed(widths), reversed(heights), reversed(generations))
301 ):
302 x = 10.0
303 # whiteSpace is the space between any two elements
304 whiteSpace = (
305 (width - currentWidth - 20) /
306 (len(generation) - 1.0 or 2.0)
307 )
308 for name in generation:
309 shape = self.__shapes[name][0]
310 shape.setPos(x, y)
311 rect = shape.sceneBoundingRect()
312 x = x + rect.width() + whiteSpace
313 y = y + currentHeight + verticalWhiteSpace
314
315 def __createAssociations(self, routes):
243 """ 316 """
244 Private method to generate the associations between the module shapes. 317 Private method to generate the associations between the module shapes.
245 318
246 @param shapes list of shapes 319 @param routes list of associations
320 @type list of tuple of (str, str)
247 """ 321 """
248 from .AssociationItem import AssociationItem, AssociationType 322 from .AssociationItem import AssociationItem, AssociationType
249 for module in list(shapes.keys()): 323 for route in routes:
250 for rel in shapes[module][1]: 324 assoc = AssociationItem(
251 assoc = AssociationItem( 325 self.__shapes[route[0]][0],
252 shapes[module][0], shapes[rel][0], 326 self.__shapes[route[1]][0],
253 AssociationType.IMPORTS, 327 AssociationType.IMPORTS,
254 colors=self.umlView.getDrawingColors()) 328 colors=self.umlView.getDrawingColors())
255 self.scene.addItem(assoc) 329 self.scene.addItem(assoc)
256 330
257 def getPersistenceData(self): 331 def getPersistenceData(self):
258 """ 332 """
259 Public method to get a string for data to be persisted. 333 Public method to get a string for data to be persisted.
260 334
261 @return persisted data string (string) 335 @return persisted data string
336 @rtype str
262 """ 337 """
263 return "package={0}, show_external={1}".format( 338 return "package={0}, show_external={1}".format(
264 self.packagePath, self.showExternalImports) 339 self.packagePath, self.showExternalImports)
265 340
266 def parsePersistenceData(self, version, data): 341 def parsePersistenceData(self, version, data):
267 """ 342 """
268 Public method to parse persisted data. 343 Public method to parse persisted data.
269 344
270 @param version version of the data (string) 345 @param version version of the data
271 @param data persisted data to be parsed (string) 346 @type str
272 @return flag indicating success (boolean) 347 @param data persisted data to be parsed
348 @type str
349 @return flag indicating success
350 @rtype bool
273 """ 351 """
274 parts = data.split(", ") 352 parts = data.split(", ")
275 if ( 353 if (
276 len(parts) != 2 or 354 len(parts) != 2 or
277 not parts[0].startswith("package=") or 355 not parts[0].startswith("package=") or
284 parts[1].split("=", 1)[1].strip()) 362 parts[1].split("=", 1)[1].strip())
285 363
286 self.initialize() 364 self.initialize()
287 365
288 return True 366 return True
367
368 def toDict(self):
369 """
370 Public method to collect data to be persisted.
371
372 @return dictionary containing data to be persisted
373 @rtype dict
374 """
375 data = {
376 "project_name": self.project.getProjectName(),
377 "show_external": self.showExternalImports,
378 }
379
380 data["package"] = (
381 Utilities.fromNativeSeparators(self.__relPackagePath)
382 if self.__relPackagePath else
383 Utilities.fromNativeSeparators(self.packagePath)
384 )
385
386 return data
387
388 def fromDict(self, version, data):
389 """
390 Public method to populate the class with data persisted by 'toDict()'.
391
392 @param version version of the data
393 @type str
394 @param data dictionary containing the persisted data
395 @type dict
396 @return tuple containing a flag indicating success and an info
397 message in case the diagram belongs to a different project
398 @rtype tuple of (bool, str)
399 """
400 try:
401 self.showExternalImports = data["show_external"]
402
403 packagePath = Utilities.toNativeSeparators(data["package"])
404 if os.path.isabs(packagePath):
405 self.packagePath = packagePath
406 self.__relPackagePath = ""
407 else:
408 # relative package paths indicate a project package
409 if data["project_name"] != self.project.getProjectName():
410 msg = self.tr(
411 "<p>The diagram belongs to project <b>{0}</b>."
412 " Please open it and try again.</p>"
413 ).format(data["project_name"])
414 return False, msg
415
416 self.__relPackagePath = packagePath
417 self.package = self.project.getAbsolutePath(packagePath)
418 except KeyError:
419 return False, ""
420
421 self.initialize()
422
423 return True, ""

eric ide

mercurial