1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2004 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the UMLItem base class.
8"""
9
10from PyQt5.QtCore import Qt, QSizeF
11from PyQt5.QtGui import QColor, QPen
12from PyQt5.QtWidgets import QGraphicsItem, QGraphicsRectItem, QStyle
13
14import Preferences
15
16
17class UMLModel:
18    """
19    Class implementing the UMLModel base class.
20    """
21    def __init__(self, name):
22        """
23        Constructor
24
25        @param name package name
26        @type str
27        """
28        self.name = name
29
30    def getName(self):
31        """
32        Public method to retrieve the model name.
33
34        @return model name
35        @rtype str
36        """
37        return self.name
38
39
40class UMLItem(QGraphicsRectItem):
41    """
42    Class implementing the UMLItem base class.
43    """
44    ItemType = "UMLItem"
45
46    def __init__(self, model=None, x=0, y=0, rounded=False, colors=None,
47                 parent=None):
48        """
49        Constructor
50
51        @param model UML model containing the item data
52        @type UMLModel
53        @param x x-coordinate
54        @type int
55        @param y y-coordinate
56        @type int
57        @param rounded flag indicating a rounded corner
58        @type bool
59        @param colors tuple containing the foreground and background colors
60        @type tuple of (QColor, QColor)
61        @param parent reference to the parent object
62        @type QGraphicsItem
63        """
64        super().__init__(parent)
65        self.model = model
66
67        if colors is None:
68            self._colors = (QColor(Qt.GlobalColor.black),
69                            QColor(Qt.GlobalColor.white))
70        else:
71            self._colors = colors
72        self.setPen(QPen(self._colors[0]))
73
74        self.font = Preferences.getGraphics("Font")
75        self.margin = 5
76        self.associations = []
77        self.shouldAdjustAssociations = False
78        self.__id = -1
79
80        self.setRect(x, y, 60, 30)
81
82        if rounded:
83            p = self.pen()
84            p.setCapStyle(Qt.PenCapStyle.RoundCap)
85            p.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
86
87        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
88        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
89        self.setFlag(
90            QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
91
92    def getName(self):
93        """
94        Public method to retrieve the item name.
95
96        @return item name
97        @rtype str
98        """
99        if self.model:
100            return self.model.name
101        else:
102            return ""
103
104    def setSize(self, width, height):
105        """
106        Public method to set the rectangles size.
107
108        @param width width of the rectangle
109        @type float
110        @param height height of the rectangle
111        @type float
112        """
113        rect = self.rect()
114        rect.setSize(QSizeF(width, height))
115        self.setRect(rect)
116
117    def addAssociation(self, assoc):
118        """
119        Public method to add an association to this widget.
120
121        @param assoc association to be added
122        @type AssociationWidget
123        """
124        if assoc and assoc not in self.associations:
125            self.associations.append(assoc)
126
127    def removeAssociation(self, assoc):
128        """
129        Public method to remove an association to this widget.
130
131        @param assoc association to be removed
132        @type AssociationWidget
133        """
134        if assoc and assoc in self.associations:
135            self.associations.remove(assoc)
136
137    def removeAssociations(self):
138        """
139        Public method to remove all associations of this widget.
140        """
141        for assoc in self.associations[:]:
142            assoc.unassociate()
143            assoc.hide()
144            del assoc
145
146    def adjustAssociations(self):
147        """
148        Public method to adjust the associations to widget movements.
149        """
150        if self.shouldAdjustAssociations:
151            for assoc in self.associations:
152                assoc.widgetMoved()
153            self.shouldAdjustAssociations = False
154
155    def moveBy(self, dx, dy):
156        """
157        Public overriden method to move the widget relative.
158
159        @param dx relative movement in x-direction
160        @type float
161        @param dy relative movement in y-direction
162        @type float
163        """
164        super().moveBy(dx, dy)
165        self.adjustAssociations()
166
167    def setPos(self, x, y):
168        """
169        Public overriden method to set the items position.
170
171        @param x absolute x-position
172        @type float
173        @param y absolute y-position
174        @type float
175        """
176        super().setPos(x, y)
177        self.adjustAssociations()
178
179    def itemChange(self, change, value):
180        """
181        Public method called when an items state changes.
182
183        @param change the item's change
184        @type QGraphicsItem.GraphicsItemChange
185        @param value the value of the change
186        @return adjusted values
187        """
188        if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
189            # 1. remember to adjust associations
190            self.shouldAdjustAssociations = True
191
192            # 2. ensure the new position is inside the scene
193            scene = self.scene()
194            if scene:
195                rect = scene.sceneRect()
196                if not rect.contains(value):
197                    # keep the item inside the scene
198                    value.setX(min(rect.right(), max(value.x(), rect.left())))
199                    value.setY(min(rect.bottom(), max(value.y(), rect.top())))
200                    return value
201
202        return QGraphicsItem.itemChange(self, change, value)
203
204    def paint(self, painter, option, widget=None):
205        """
206        Public method to paint the item in local coordinates.
207
208        @param painter reference to the painter object
209        @type QPainter
210        @param option style options
211        @type QStyleOptionGraphicsItem
212        @param widget optional reference to the widget painted on
213        @type QWidget
214        """
215        pen = self.pen()
216        if (
217            (option.state & QStyle.StateFlag.State_Selected) ==
218            QStyle.State(QStyle.StateFlag.State_Selected)
219        ):
220            pen.setWidth(2)
221        else:
222            pen.setWidth(1)
223
224        painter.setPen(pen)
225        painter.setBrush(self.brush())
226        painter.drawRect(self.rect())
227        self.adjustAssociations()
228
229    def setId(self, itemId):
230        """
231        Public method to assign an ID to the item.
232
233        @param itemId assigned ID
234        @type int
235        """
236        self.__id = itemId
237
238    def getId(self):
239        """
240        Public method to get the item ID.
241
242        @return ID of the item
243        @rtype int
244        """
245        return self.__id
246
247    def getItemType(self):
248        """
249        Public method to get the item's type.
250
251        @return item type
252        @rtype str
253        """
254        return self.ItemType
255
256    def buildItemDataString(self):
257        """
258        Public method to build a string to persist the specific item data.
259
260        This string must start with ", " and should be built like
261        "attribute=value" with pairs separated by ", ". value must not
262        contain ", " or newlines.
263
264        @return persistence data
265        @rtype str
266        """
267        return ""
268
269    def parseItemDataString(self, version, data):
270        """
271        Public method to parse the given persistence data.
272
273        @param version version of the data
274        @type str
275        @param data persisted data to be parsed
276        @type str
277        @return flag indicating success
278        @rtype bool
279        """
280        return True
281
282    def toDict(self):
283        """
284        Public method to collect data to be persisted.
285
286        @return dictionary containing data to be persisted
287        @rtype dict
288        """
289        return {
290            "id": self.getId(),
291            "x": self.x(),
292            "y": self.y(),
293            "type": self.getItemType(),
294            "model_name": self.model.getName(),
295        }
296
297    @classmethod
298    def fromDict(cls, data, colors=None):
299        """
300        Class method to create a generic UML item from persisted data.
301
302        @param data dictionary containing the persisted data as generated
303            by toDict()
304        @type dict
305        @param colors tuple containing the foreground and background colors
306        @type tuple of (QColor, QColor)
307        @return created UML item
308        @rtype UMLItem
309        """
310        try:
311            model = UMLModel(data["model_name"])
312            itm = cls(model=model,
313                      x=0,
314                      y=0,
315                      colors=colors)
316            itm.setPos(data["x"], data["y"])
317            itm.setId(data["id"])
318            return itm
319        except KeyError:
320            return None
321