|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2004 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a graphics item for an association between two items. |
|
8 """ |
|
9 |
|
10 import enum |
|
11 |
|
12 from PyQt6.QtCore import QPointF, QRectF, QLineF |
|
13 from PyQt6.QtWidgets import QGraphicsItem |
|
14 |
|
15 from EricGraphics.EricArrowItem import EricArrowItem, EricArrowType |
|
16 |
|
17 import Utilities |
|
18 |
|
19 |
|
20 class AssociationType(enum.Enum): |
|
21 """ |
|
22 Class defining the association types. |
|
23 """ |
|
24 NORMAL = 0 |
|
25 GENERALISATION = 1 |
|
26 IMPORTS = 2 |
|
27 |
|
28 |
|
29 class AssociationPointRegion(enum.Enum): |
|
30 """ |
|
31 Class defining the regions for an association end point. |
|
32 """ |
|
33 NO_REGION = 0 |
|
34 WEST = 1 |
|
35 NORTH = 2 |
|
36 EAST = 3 |
|
37 SOUTH = 4 |
|
38 NORTH_WEST = 5 |
|
39 NORTH_EAST = 6 |
|
40 SOUTH_EAST = 7 |
|
41 SOUTH_WEST = 8 |
|
42 CENTER = 9 |
|
43 |
|
44 |
|
45 class AssociationItem(EricArrowItem): |
|
46 """ |
|
47 Class implementing a graphics item for an association between two items. |
|
48 |
|
49 The association is drawn as an arrow starting at the first items and |
|
50 ending at the second. |
|
51 """ |
|
52 def __init__(self, itemA, itemB, assocType=AssociationType.NORMAL, |
|
53 topToBottom=False, colors=None, parent=None): |
|
54 """ |
|
55 Constructor |
|
56 |
|
57 @param itemA first widget of the association |
|
58 @type UMLItem |
|
59 @param itemB second widget of the association |
|
60 @type UMLItem |
|
61 @param assocType type of the association |
|
62 @type AssociationType |
|
63 @param topToBottom flag indicating to draw the association |
|
64 from item A top to item B bottom |
|
65 @type bool |
|
66 @param colors tuple containing the foreground and background colors |
|
67 @type tuple of (QColor, QColor) |
|
68 @param parent reference to the parent object |
|
69 @type QGraphicsItem |
|
70 """ |
|
71 if assocType in (AssociationType.NORMAL, AssociationType.IMPORTS): |
|
72 arrowType = EricArrowType.NORMAL |
|
73 arrowFilled = True |
|
74 elif assocType == AssociationType.GENERALISATION: |
|
75 arrowType = EricArrowType.WIDE |
|
76 arrowFilled = False |
|
77 |
|
78 EricArrowItem.__init__(self, QPointF(0, 0), QPointF(100, 100), |
|
79 arrowFilled, arrowType, colors, parent) |
|
80 |
|
81 self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False) |
|
82 self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False) |
|
83 |
|
84 if topToBottom: |
|
85 self.calculateEndingPoints = ( |
|
86 self.__calculateEndingPoints_topToBottom |
|
87 ) |
|
88 else: |
|
89 #- self.calculateEndingPoints = self.__calculateEndingPoints_center |
|
90 self.calculateEndingPoints = self.__calculateEndingPoints_rectangle |
|
91 |
|
92 self.itemA = itemA |
|
93 self.itemB = itemB |
|
94 self.assocType = assocType |
|
95 self.topToBottom = topToBottom |
|
96 |
|
97 self.regionA = AssociationPointRegion.NO_REGION |
|
98 self.regionB = AssociationPointRegion.NO_REGION |
|
99 |
|
100 self.calculateEndingPoints() |
|
101 |
|
102 self.itemA.addAssociation(self) |
|
103 self.itemB.addAssociation(self) |
|
104 |
|
105 def __mapRectFromItem(self, item): |
|
106 """ |
|
107 Private method to map item's rectangle to this item's coordinate |
|
108 system. |
|
109 |
|
110 @param item reference to the item to be mapped |
|
111 @type QGraphicsRectItem |
|
112 @return item's rectangle in local coordinates |
|
113 @rtype QRectF |
|
114 """ |
|
115 rect = item.rect() |
|
116 tl = self.mapFromItem(item, rect.topLeft()) |
|
117 return QRectF(tl.x(), tl.y(), rect.width(), rect.height()) |
|
118 |
|
119 def __calculateEndingPoints_topToBottom(self): |
|
120 """ |
|
121 Private method to calculate the ending points of the association item. |
|
122 |
|
123 The ending points are calculated from the top center of the lower item |
|
124 to the bottom center of the upper item. |
|
125 """ |
|
126 if self.itemA is None or self.itemB is None: |
|
127 return |
|
128 |
|
129 self.prepareGeometryChange() |
|
130 |
|
131 rectA = self.__mapRectFromItem(self.itemA) |
|
132 rectB = self.__mapRectFromItem(self.itemB) |
|
133 midA = QPointF(rectA.x() + rectA.width() / 2.0, |
|
134 rectA.y() + rectA.height() / 2.0) |
|
135 midB = QPointF(rectB.x() + rectB.width() / 2.0, |
|
136 rectB.y() + rectB.height() / 2.0) |
|
137 if midA.y() > midB.y(): |
|
138 startP = QPointF(rectA.x() + rectA.width() / 2.0, rectA.y()) |
|
139 endP = QPointF(rectB.x() + rectB.width() / 2.0, |
|
140 rectB.y() + rectB.height()) |
|
141 else: |
|
142 startP = QPointF(rectA.x() + rectA.width() / 2.0, |
|
143 rectA.y() + rectA.height()) |
|
144 endP = QPointF(rectB.x() + rectB.width() / 2.0, rectB.y()) |
|
145 self.setPoints(startP.x(), startP.y(), endP.x(), endP.y()) |
|
146 |
|
147 def __calculateEndingPoints_center(self): |
|
148 """ |
|
149 Private method to calculate the ending points of the association item. |
|
150 |
|
151 The ending points are calculated from the centers of the |
|
152 two associated items. |
|
153 """ |
|
154 if self.itemA is None or self.itemB is None: |
|
155 return |
|
156 |
|
157 self.prepareGeometryChange() |
|
158 |
|
159 rectA = self.__mapRectFromItem(self.itemA) |
|
160 rectB = self.__mapRectFromItem(self.itemB) |
|
161 midA = QPointF(rectA.x() + rectA.width() / 2.0, |
|
162 rectA.y() + rectA.height() / 2.0) |
|
163 midB = QPointF(rectB.x() + rectB.width() / 2.0, |
|
164 rectB.y() + rectB.height() / 2.0) |
|
165 startP = self.__findRectIntersectionPoint(self.itemA, midA, midB) |
|
166 endP = self.__findRectIntersectionPoint(self.itemB, midB, midA) |
|
167 |
|
168 if ( |
|
169 startP.x() != -1 and |
|
170 startP.y() != -1 and |
|
171 endP.x() != -1 and |
|
172 endP.y() != -1 |
|
173 ): |
|
174 self.setPoints(startP.x(), startP.y(), endP.x(), endP.y()) |
|
175 |
|
176 def __calculateEndingPoints_rectangle(self): |
|
177 r""" |
|
178 Private method to calculate the ending points of the association item. |
|
179 |
|
180 The ending points are calculated by the following method. |
|
181 |
|
182 For each item the diagram is divided in four Regions by its diagonals |
|
183 as indicated below |
|
184 <pre> |
|
185 +------------------------------+ |
|
186 | \ Region 2 / | |
|
187 | \ / | |
|
188 | |--------| | |
|
189 | | \ / | | |
|
190 | | \ / | | |
|
191 | | \/ | | |
|
192 | Region 1 | /\ | Region 3 | |
|
193 | | / \ | | |
|
194 | | / \ | | |
|
195 | |--------| | |
|
196 | / \ | |
|
197 | / Region 4 \ | |
|
198 +------------------------------+ |
|
199 </pre> |
|
200 |
|
201 Each diagonal is defined by two corners of the bounding rectangle. |
|
202 |
|
203 To calculate the start point we have to find out in which |
|
204 region (defined by itemA's diagonals) is itemB's TopLeft corner |
|
205 (lets call it region M). After that the start point will be |
|
206 the middle point of rectangle's side contained in region M. |
|
207 |
|
208 To calculate the end point we repeat the above but in the opposite |
|
209 direction (from itemB to itemA) |
|
210 """ |
|
211 if self.itemA is None or self.itemB is None: |
|
212 return |
|
213 |
|
214 self.prepareGeometryChange() |
|
215 |
|
216 rectA = self.__mapRectFromItem(self.itemA) |
|
217 rectB = self.__mapRectFromItem(self.itemB) |
|
218 |
|
219 xA = rectA.x() + rectA.width() / 2.0 |
|
220 yA = rectA.y() + rectA.height() / 2.0 |
|
221 xB = rectB.x() + rectB.width() / 2.0 |
|
222 yB = rectB.y() + rectB.height() / 2.0 |
|
223 |
|
224 # find itemA region |
|
225 rc = QRectF(xA, yA, rectA.width(), rectA.height()) |
|
226 self.regionA = self.__findPointRegion(rc, xB, yB) |
|
227 # move some regions to the standard ones |
|
228 if self.regionA == AssociationPointRegion.NORTH_WEST: |
|
229 self.regionA = AssociationPointRegion.NORTH |
|
230 elif self.regionA == AssociationPointRegion.NORTH_EAST: |
|
231 self.regionA = AssociationPointRegion.EAST |
|
232 elif self.regionA == AssociationPointRegion.SOUTH_EAST: |
|
233 self.regionA = AssociationPointRegion.SOUTH |
|
234 elif self.regionA in ( |
|
235 AssociationPointRegion.SOUTH_WEST, |
|
236 AssociationPointRegion.CENTER |
|
237 ): |
|
238 self.regionA = AssociationPointRegion.WEST |
|
239 |
|
240 self.__updateEndPoint(self.regionA, True) |
|
241 |
|
242 # now do the same for itemB |
|
243 rc = QRectF(xB, yB, rectB.width(), rectB.height()) |
|
244 self.regionB = self.__findPointRegion(rc, xA, yA) |
|
245 # move some regions to the standard ones |
|
246 if self.regionB == AssociationPointRegion.NORTH_WEST: |
|
247 self.regionB = AssociationPointRegion.NORTH |
|
248 elif self.regionB == AssociationPointRegion.NORTH_EAST: |
|
249 self.regionB = AssociationPointRegion.EAST |
|
250 elif self.regionB == AssociationPointRegion.SOUTH_EAST: |
|
251 self.regionB = AssociationPointRegion.SOUTH |
|
252 elif self.regionB in ( |
|
253 AssociationPointRegion.SOUTH_WEST, |
|
254 AssociationPointRegion.CENTER |
|
255 ): |
|
256 self.regionB = AssociationPointRegion.WEST |
|
257 |
|
258 self.__updateEndPoint(self.regionB, False) |
|
259 |
|
260 def __findPointRegion(self, rect, posX, posY): |
|
261 """ |
|
262 Private method to find out, which region of rectangle rect contains |
|
263 the point (PosX, PosY) and returns the region number. |
|
264 |
|
265 @param rect rectangle to calculate the region for |
|
266 @type QRectF |
|
267 @param posX x position of point |
|
268 @type float |
|
269 @param posY y position of point |
|
270 @type float |
|
271 @return the calculated region number<br /> |
|
272 West = Region 1<br /> |
|
273 North = Region 2<br /> |
|
274 East = Region 3<br /> |
|
275 South = Region 4<br /> |
|
276 NorthWest = On diagonal 2 between Region 1 and 2<br /> |
|
277 NorthEast = On diagonal 1 between Region 2 and 3<br /> |
|
278 SouthEast = On diagonal 2 between Region 3 and 4<br /> |
|
279 SouthWest = On diagonal 1 between Region4 and 1<br /> |
|
280 Center = On diagonal 1 and On diagonal 2 (the center)<br /> |
|
281 @rtype AssociationPointRegion |
|
282 """ |
|
283 w = rect.width() |
|
284 h = rect.height() |
|
285 x = rect.x() |
|
286 y = rect.y() |
|
287 slope2 = w / h |
|
288 slope1 = -slope2 |
|
289 b1 = x + w / 2.0 - y * slope1 |
|
290 b2 = x + w / 2.0 - y * slope2 |
|
291 |
|
292 eval1 = slope1 * posY + b1 |
|
293 eval2 = slope2 * posY + b2 |
|
294 |
|
295 result = AssociationPointRegion.NO_REGION |
|
296 |
|
297 # inside region 1 |
|
298 if eval1 > posX and eval2 > posX: |
|
299 result = AssociationPointRegion.WEST |
|
300 |
|
301 #inside region 2 |
|
302 elif eval1 > posX and eval2 < posX: |
|
303 result = AssociationPointRegion.NORTH |
|
304 |
|
305 # inside region 3 |
|
306 elif eval1 < posX and eval2 < posX: |
|
307 result = AssociationPointRegion.EAST |
|
308 |
|
309 # inside region 4 |
|
310 elif eval1 < posX and eval2 > posX: |
|
311 result = AssociationPointRegion.SOUTH |
|
312 |
|
313 # inside region 5 |
|
314 elif eval1 == posX and eval2 < posX: |
|
315 result = AssociationPointRegion.NORTH_WEST |
|
316 |
|
317 # inside region 6 |
|
318 elif eval1 < posX and eval2 == posX: |
|
319 result = AssociationPointRegion.NORTH_EAST |
|
320 |
|
321 # inside region 7 |
|
322 elif eval1 == posX and eval2 > posX: |
|
323 result = AssociationPointRegion.SOUTH_EAST |
|
324 |
|
325 # inside region 8 |
|
326 elif eval1 > posX and eval2 == posX: |
|
327 result = AssociationPointRegion.SOUTH_WEST |
|
328 |
|
329 # inside region 9 |
|
330 elif eval1 == posX and eval2 == posX: |
|
331 result = AssociationPointRegion.CENTER |
|
332 |
|
333 return result |
|
334 |
|
335 def __updateEndPoint(self, region, isWidgetA): |
|
336 """ |
|
337 Private method to update an endpoint. |
|
338 |
|
339 @param region the region for the endpoint |
|
340 @type AssociationPointRegion |
|
341 @param isWidgetA flag indicating update for itemA is done |
|
342 @type bool |
|
343 """ |
|
344 if region == AssociationPointRegion.NO_REGION: |
|
345 return |
|
346 |
|
347 rect = ( |
|
348 self.__mapRectFromItem(self.itemA) |
|
349 if isWidgetA else |
|
350 self.__mapRectFromItem(self.itemB) |
|
351 ) |
|
352 x = rect.x() |
|
353 y = rect.y() |
|
354 ww = rect.width() |
|
355 wh = rect.height() |
|
356 ch = wh / 2.0 |
|
357 cw = ww / 2.0 |
|
358 |
|
359 if region == AssociationPointRegion.WEST: |
|
360 px = x |
|
361 py = y + ch |
|
362 elif region == AssociationPointRegion.NORTH: |
|
363 px = x + cw |
|
364 py = y |
|
365 elif region == AssociationPointRegion.EAST: |
|
366 px = x + ww |
|
367 py = y + ch |
|
368 elif region in ( |
|
369 AssociationPointRegion.SOUTH, |
|
370 AssociationPointRegion.CENTER |
|
371 ): |
|
372 px = x + cw |
|
373 py = y + wh |
|
374 |
|
375 if isWidgetA: |
|
376 self.setStartPoint(px, py) |
|
377 else: |
|
378 self.setEndPoint(px, py) |
|
379 |
|
380 def __findRectIntersectionPoint(self, item, p1, p2): |
|
381 """ |
|
382 Private method to find the intersection point of a line with a |
|
383 rectangle. |
|
384 |
|
385 @param item item to check against |
|
386 @type UMLItem |
|
387 @param p1 first point of the line |
|
388 @type QPointF |
|
389 @param p2 second point of the line |
|
390 @type QPointF |
|
391 @return the intersection point |
|
392 @rtype QPointF |
|
393 """ |
|
394 rect = self.__mapRectFromItem(item) |
|
395 lines = [ |
|
396 QLineF(rect.topLeft(), rect.topRight()), |
|
397 QLineF(rect.topLeft(), rect.bottomLeft()), |
|
398 QLineF(rect.bottomRight(), rect.bottomLeft()), |
|
399 QLineF(rect.bottomRight(), rect.topRight()) |
|
400 ] |
|
401 intersectLine = QLineF(p1, p2) |
|
402 intersectPoint = QPointF(0, 0) |
|
403 for line in lines: |
|
404 if ( |
|
405 intersectLine.intersect(line, intersectPoint) == |
|
406 QLineF.IntersectType.BoundedIntersection |
|
407 ): |
|
408 return intersectPoint |
|
409 return QPointF(-1.0, -1.0) |
|
410 |
|
411 def __findIntersection(self, p1, p2, p3, p4): |
|
412 """ |
|
413 Private method to calculate the intersection point of two lines. |
|
414 |
|
415 The first line is determined by the points p1 and p2, the second |
|
416 line by p3 and p4. If the intersection point is not contained in |
|
417 the segment p1p2, then it returns (-1.0, -1.0). |
|
418 |
|
419 For the function's internal calculations remember:<br /> |
|
420 QT coordinates start with the point (0,0) as the topleft corner |
|
421 and x-values increase from left to right and y-values increase |
|
422 from top to bottom; it means the visible area is quadrant I in |
|
423 the regular XY coordinate system |
|
424 |
|
425 <pre> |
|
426 Quadrant II | Quadrant I |
|
427 -----------------|----------------- |
|
428 Quadrant III | Quadrant IV |
|
429 </pre> |
|
430 |
|
431 In order for the linear function calculations to work in this method |
|
432 we must switch x and y values (x values become y values and viceversa) |
|
433 |
|
434 @param p1 first point of first line |
|
435 @type QPointF |
|
436 @param p2 second point of first line |
|
437 @type QPointF |
|
438 @param p3 first point of second line |
|
439 @type QPointF |
|
440 @param p4 second point of second line |
|
441 @type QPointF |
|
442 @return the intersection point |
|
443 @rtype QPointF |
|
444 """ |
|
445 x1 = p1.y() |
|
446 y1 = p1.x() |
|
447 x2 = p2.y() |
|
448 y2 = p2.x() |
|
449 x3 = p3.y() |
|
450 y3 = p3.x() |
|
451 x4 = p4.y() |
|
452 y4 = p4.x() |
|
453 |
|
454 # line 1 is the line between (x1, y1) and (x2, y2) |
|
455 # line 2 is the line between (x3, y3) and (x4, y4) |
|
456 no_line1 = True # it is false, if line 1 is a linear function |
|
457 no_line2 = True # it is false, if line 2 is a linear function |
|
458 slope1 = 0.0 |
|
459 slope2 = 0.0 |
|
460 b1 = 0.0 |
|
461 b2 = 0.0 |
|
462 |
|
463 if x2 != x1: |
|
464 slope1 = (y2 - y1) / (x2 - x1) |
|
465 b1 = y1 - slope1 * x1 |
|
466 no_line1 = False |
|
467 if x4 != x3: |
|
468 slope2 = (y4 - y3) / (x4 - x3) |
|
469 b2 = y3 - slope2 * x3 |
|
470 no_line2 = False |
|
471 |
|
472 pt = QPointF() |
|
473 # if either line is not a function |
|
474 if no_line1 and no_line2: |
|
475 # if the lines are not the same one |
|
476 if x1 != x3: |
|
477 return QPointF(-1.0, -1.0) |
|
478 # if the lines are the same ones |
|
479 if y3 <= y4: |
|
480 if y3 <= y1 and y1 <= y4: |
|
481 return QPointF(y1, x1) |
|
482 else: |
|
483 return QPointF(y2, x2) |
|
484 else: |
|
485 if y4 <= y1 and y1 <= y3: |
|
486 return QPointF(y1, x1) |
|
487 else: |
|
488 return QPointF(y2, x2) |
|
489 elif no_line1: |
|
490 pt.setX(slope2 * x1 + b2) |
|
491 pt.setY(x1) |
|
492 if y1 >= y2: |
|
493 if not (y2 <= pt.x() and pt.x() <= y1): |
|
494 pt.setX(-1.0) |
|
495 pt.setY(-1.0) |
|
496 else: |
|
497 if not (y1 <= pt.x() and pt.x() <= y2): |
|
498 pt.setX(-1.0) |
|
499 pt.setY(-1.0) |
|
500 return pt |
|
501 elif no_line2: |
|
502 pt.setX(slope1 * x3 + b1) |
|
503 pt.setY(x3) |
|
504 if y3 >= y4: |
|
505 if not (y4 <= pt.x() and pt.x() <= y3): |
|
506 pt.setX(-1.0) |
|
507 pt.setY(-1.0) |
|
508 else: |
|
509 if not (y3 <= pt.x() and pt.x() <= y4): |
|
510 pt.setX(-1.0) |
|
511 pt.setY(-1.0) |
|
512 return pt |
|
513 |
|
514 if slope1 == slope2: |
|
515 pt.setX(-1.0) |
|
516 pt.setY(-1.0) |
|
517 return pt |
|
518 |
|
519 pt.setY((b2 - b1) / (slope1 - slope2)) |
|
520 pt.setX(slope1 * pt.y() + b1) |
|
521 # the intersection point must be inside the segment (x1, y1) (x2, y2) |
|
522 if x2 >= x1 and y2 >= y1: |
|
523 if not ((x1 <= pt.y() and pt.y() <= x2) and |
|
524 (y1 <= pt.x() and pt.x() <= y2)): |
|
525 pt.setX(-1.0) |
|
526 pt.setY(-1.0) |
|
527 elif x2 < x1 and y2 >= y1: |
|
528 if not ((x2 <= pt.y() and pt.y() <= x1) and |
|
529 (y1 <= pt.x() and pt.x() <= y2)): |
|
530 pt.setX(-1.0) |
|
531 pt.setY(-1.0) |
|
532 elif x2 >= x1 and y2 < y1: |
|
533 if not ((x1 <= pt.y() and pt.y() <= x2) and |
|
534 (y2 <= pt.x() and pt.x() <= y1)): |
|
535 pt.setX(-1.0) |
|
536 pt.setY(-1.0) |
|
537 else: |
|
538 if not ((x2 <= pt.y() and pt.y() <= x1) and |
|
539 (y2 <= pt.x() and pt.x() <= y1)): |
|
540 pt.setX(-1.0) |
|
541 pt.setY(-1.0) |
|
542 |
|
543 return pt |
|
544 |
|
545 def widgetMoved(self): |
|
546 """ |
|
547 Public method to recalculate the association after a widget was moved. |
|
548 """ |
|
549 self.calculateEndingPoints() |
|
550 |
|
551 def unassociate(self): |
|
552 """ |
|
553 Public method to unassociate from the widgets. |
|
554 """ |
|
555 self.itemA.removeAssociation(self) |
|
556 self.itemB.removeAssociation(self) |
|
557 |
|
558 @classmethod |
|
559 def parseAssociationItemDataString(cls, data): |
|
560 """ |
|
561 Class method to parse the given persistence data. |
|
562 |
|
563 @param data persisted data to be parsed |
|
564 @type str |
|
565 @return tuple with the IDs of the source and destination items, |
|
566 the association type and a flag indicating to associate from top |
|
567 to bottom |
|
568 @rtype tuple of (int, int, int, bool) |
|
569 """ |
|
570 src = -1 |
|
571 dst = -1 |
|
572 assocType = AssociationType.NORMAL |
|
573 topToBottom = False |
|
574 for entry in data.split(", "): |
|
575 if "=" in entry: |
|
576 key, value = entry.split("=", 1) |
|
577 if key == "src": |
|
578 src = int(value) |
|
579 elif key == "dst": |
|
580 dst = int(value) |
|
581 elif key == "type": |
|
582 assocType = AssociationType(int(value)) |
|
583 elif key == "topToBottom": |
|
584 topToBottom = Utilities.toBool(value) |
|
585 |
|
586 return src, dst, assocType, topToBottom |
|
587 |
|
588 def toDict(self): |
|
589 """ |
|
590 Public method to collect data to be persisted. |
|
591 |
|
592 @return dictionary containing data to be persisted |
|
593 @rtype dict |
|
594 """ |
|
595 return { |
|
596 "src": self.itemA.getId(), |
|
597 "dst": self.itemB.getId(), |
|
598 "type": self.assocType.value, |
|
599 "topToBottom": self.topToBottom, |
|
600 } |
|
601 |
|
602 @classmethod |
|
603 def fromDict(cls, data, umlItems, colors=None): |
|
604 """ |
|
605 Class method to create an association item from persisted data. |
|
606 |
|
607 @param data dictionary containing the persisted data as generated |
|
608 by toDict() |
|
609 @type dict |
|
610 @param umlItems list of UML items |
|
611 @type list of UMLItem |
|
612 @param colors tuple containing the foreground and background colors |
|
613 @type tuple of (QColor, QColor) |
|
614 @return created association item |
|
615 @rtype AssociationItem |
|
616 """ |
|
617 try: |
|
618 return cls(umlItems[data["src"]], |
|
619 umlItems[data["dst"]], |
|
620 assocType=AssociationType(data["type"]), |
|
621 topToBottom=data["topToBottom"], |
|
622 colors=colors) |
|
623 except (KeyError, ValueError): |
|
624 return None |