source: orange/Orange/OrangeCanvas/canvas/items/controlpoints.py @ 11160:6bfea7812243

Revision 11160:6bfea7812243, 12.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Refactored GraphicsPathObject and ControlPoint/Rect/Line into two new modules.

Line 
1import logging
2
3from PyQt4.QtGui import QGraphicsItem, QGraphicsObject, QBrush, QPainterPath
4from PyQt4.QtCore import Qt, QPointF, QLineF, QRectF, QMargins, QVariant, \
5                         QEvent
6
7from PyQt4.QtCore import pyqtSignal as Signal, pyqtProperty as Property
8
9from .graphicspathobject import GraphicsPathObject
10
11log = logging.getLogger(__name__)
12
13
14class ControlPoint(GraphicsPathObject):
15    """A control point for annotations in the canvas.
16    """
17    Free = 0
18
19    Left, Top, Right, Bottom, Center = 1, 2, 4, 8, 16
20
21    TopLeft = Top | Left
22    TopRight = Top | Right
23    BottomRight = Bottom | Right
24    BottomLeft = Bottom | Left
25
26    def __init__(self, parent=None, anchor=0, **kwargs):
27        GraphicsPathObject.__init__(self, parent, **kwargs)
28        self.setFlag(QGraphicsItem.ItemIsMovable)
29        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, False)
30        self.setAcceptedMouseButtons(Qt.LeftButton)
31
32        self.__constraint = 0
33        self.__constraintFunc = None
34        self.__anchor = 0
35        self.setAnchor(anchor)
36
37        path = QPainterPath()
38        path.addEllipse(QRectF(-4, -4, 8, 8))
39        self.setPath(path)
40
41        self.setBrush(QBrush(Qt.lightGray, Qt.SolidPattern))
42
43    def setAnchor(self, anchor):
44        """Set anchor position
45        """
46        self.__anchor = anchor
47
48    def anchor(self):
49        return self.__anchor
50
51    def mousePressEvent(self, event):
52        if event.button() == Qt.LeftButton:
53            # Enable ItemPositionChange (and pos constraint) only when
54            # this is the mouse grabber item
55            self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
56        return GraphicsPathObject.mousePressEvent(self, event)
57
58    def mouseReleaseEvent(self, event):
59        if event.button() == Qt.LeftButton:
60            self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, False)
61        return GraphicsPathObject.mouseReleaseEvent(self, event)
62
63    def itemChange(self, change, value):
64
65        if change == QGraphicsItem.ItemPositionChange:
66            pos = value.toPointF()
67            newpos = self.constrain(pos)
68            return QVariant(newpos)
69
70        return GraphicsPathObject.itemChange(self, change, value)
71
72    def hasConstraint(self):
73        return self.__constraintFunc is not None or self.__constraint != 0
74
75    def setConstraint(self, constraint):
76        """Set the constraint for the point (Qt.Vertical Qt.Horizontal or 0)
77
78        .. note:: Clears the constraintFunc if it was previously set
79
80        """
81        if self.__constraint != constraint:
82            self.__constraint = constraint
83
84        self.__constraintFunc = None
85
86    def constrain(self, pos):
87        """Constrain the pos.
88        """
89        if self.__constraintFunc:
90            return self.__constraintFunc(pos)
91        elif self.__constraint == Qt.Vertical:
92            return QPointF(self.pos().x(), pos.y())
93        elif self.__constraint == Qt.Horizontal:
94            return QPointF(pos.x(), self.pos().y())
95        else:
96            return pos
97
98    def setConstraintFunc(self, func):
99        if self.__constraintFunc != func:
100            self.__constraintFunc = func
101
102
103class ControlPointRect(QGraphicsObject):
104    Free = 0
105    KeepAspectRatio = 1
106    KeepCenter = 2
107
108    rectChanged = Signal(QRectF)
109    rectEdited = Signal(QRectF)
110
111    def __init__(self, parent=None, rect=None, constraints=0, **kwargs):
112        QGraphicsObject.__init__(self, parent, **kwargs)
113        self.setFlag(QGraphicsItem.ItemHasNoContents)
114
115        self.__rect = rect if rect is not None else QRectF()
116        self.__margins = QMargins()
117        points = \
118            [ControlPoint(self, ControlPoint.Left),
119             ControlPoint(self, ControlPoint.Top),
120             ControlPoint(self, ControlPoint.TopLeft),
121             ControlPoint(self, ControlPoint.Right),
122             ControlPoint(self, ControlPoint.TopRight),
123             ControlPoint(self, ControlPoint.Bottom),
124             ControlPoint(self, ControlPoint.BottomLeft),
125             ControlPoint(self, ControlPoint.BottomRight)
126             ]
127        assert(points == sorted(points, key=lambda p: p.anchor()))
128
129        self.__points = dict((p.anchor(), p) for p in points)
130
131        if self.scene():
132            self.__installFilter()
133
134        self.controlPoint(ControlPoint.Top).setConstraint(Qt.Vertical)
135        self.controlPoint(ControlPoint.Bottom).setConstraint(Qt.Vertical)
136        self.controlPoint(ControlPoint.Left).setConstraint(Qt.Horizontal)
137        self.controlPoint(ControlPoint.Right).setConstraint(Qt.Horizontal)
138
139        self.__constraints = constraints
140        self.__activeControl = None
141
142        self.__pointsLayout()
143
144    def controlPoint(self, anchor):
145        """Return the anchor point at anchor position if not set.
146        """
147        return self.__points.get(anchor)
148
149    def setRect(self, rect):
150        if self.__rect != rect:
151            self.__rect = rect
152            self.__pointsLayout()
153            self.prepareGeometryChange()
154            self.rectChanged.emit(rect)
155
156    def rect(self):
157        """Return the control rect
158        """
159        # Return the rect normalized. During the control point move the
160        # rect can change to an invalid size, but the layout must still
161        # know to which point does an unnormalized rect side belong.
162        return self.__rect.normalized()
163
164    rect_ = Property(QRectF, fget=rect, fset=setRect, user=True)
165
166    def setControlMargins(self, *margins):
167        """Set the controls points on the margins around `rect`
168        """
169        if len(margins) > 1:
170            margins = QMargins(*margins)
171        else:
172            margins = margins[0]
173            if isinstance(margins, int):
174                margins = QMargins(margins, margins, margins, margins)
175
176        if self.__margins != margins:
177            self.__margins = margins
178            self.__pointsLayout()
179
180    def controlMargins(self):
181        return self.__margins
182
183    def setConstraints(self, constraints):
184        pass
185
186    def itemChange(self, change, value):
187        if change == QGraphicsItem.ItemSceneHasChanged and self.scene():
188            self.__installFilter()
189
190        return QGraphicsObject.itemChange(self, change, value)
191
192    def sceneEventFilter(self, obj, event):
193        try:
194            if isinstance(obj, ControlPoint):
195                etype = event.type()
196                if etype == QEvent.GraphicsSceneMousePress and \
197                        event.button() == Qt.LeftButton:
198                    self.__setActiveControl(obj)
199
200                elif etype == QEvent.GraphicsSceneMouseRelease and \
201                        event.button() == Qt.LeftButton:
202                    self.__setActiveControl(None)
203
204        except Exception:
205            log.error("Error in 'ControlPointRect.sceneEventFilter'",
206                      exc_info=True)
207
208        return QGraphicsObject.sceneEventFilter(self, obj, event)
209
210    def __installFilter(self):
211        # Install filters on the control points.
212        try:
213            for p in self.__points.values():
214                p.installSceneEventFilter(self)
215        except Exception:
216            log.error("Error in ControlPointRect.__installFilter",
217                      exc_info=True)
218
219    def __pointsLayout(self):
220        """Layout the control points
221        """
222        rect = self.__rect
223        margins = self.__margins
224        rect = rect.adjusted(-margins.left(), -margins.top(),
225                             margins.right(), margins.bottom())
226        center = rect.center()
227        cx, cy = center.x(), center.y()
228        left, top, right, bottom = \
229                rect.left(), rect.top(), rect.right(), rect.bottom()
230
231        self.controlPoint(ControlPoint.Left).setPos(left, cy)
232        self.controlPoint(ControlPoint.Right).setPos(right, cy)
233        self.controlPoint(ControlPoint.Top).setPos(cx, top)
234        self.controlPoint(ControlPoint.Bottom).setPos(cx, bottom)
235
236        self.controlPoint(ControlPoint.TopLeft).setPos(left, top)
237        self.controlPoint(ControlPoint.TopRight).setPos(right, top)
238        self.controlPoint(ControlPoint.BottomLeft).setPos(left, bottom)
239        self.controlPoint(ControlPoint.BottomRight).setPos(right, bottom)
240
241    def __setActiveControl(self, control):
242        if self.__activeControl != control:
243            if self.__activeControl is not None:
244                self.__activeControl.positionChanged[QPointF].disconnect(
245                    self.__activeControlMoved
246                )
247
248            self.__activeControl = control
249
250            if control is not None:
251                control.positionChanged[QPointF].connect(
252                    self.__activeControlMoved
253                )
254
255    def __activeControlMoved(self, pos):
256        # The active control point has moved, update the control
257        # rectangle
258        control = self.__activeControl
259        pos = control.pos()
260        rect = QRectF(self.__rect)
261        margins = self.__margins
262
263        # TODO: keyboard modifiers and constraints.
264
265        anchor = control.anchor()
266        if anchor & ControlPoint.Top:
267            rect.setTop(pos.y() + margins.top())
268        elif anchor & ControlPoint.Bottom:
269            rect.setBottom(pos.y() - margins.bottom())
270
271        if anchor & ControlPoint.Left:
272            rect.setLeft(pos.x() + margins.left())
273        elif anchor & ControlPoint.Right:
274            rect.setRight(pos.x() - margins.right())
275
276        changed = self.__rect != rect
277
278        self.blockSignals(True)
279        self.setRect(rect)
280        self.blockSignals(False)
281
282        if changed:
283            self.rectEdited.emit(rect)
284
285    def boundingRect(self):
286        return QRectF()
287
288
289class ControlPointLine(QGraphicsObject):
290
291    lineChanged = Signal(QLineF)
292    lineEdited = Signal(QLineF)
293
294    def __init__(self, parent=None, **kwargs):
295        QGraphicsObject.__init__(self, parent, **kwargs)
296        self.setFlag(QGraphicsItem.ItemHasNoContents)
297
298        self.__line = QLineF()
299        self.__points = \
300            [ControlPoint(self, ControlPoint.TopLeft),  # TopLeft is line start
301             ControlPoint(self, ControlPoint.BottomRight)  # line end
302             ]
303
304        self.__activeControl = None
305
306        if self.scene():
307            self.__installFilter()
308
309    def setLine(self, line):
310        if not isinstance(line, QLineF):
311            raise TypeError()
312
313        if line != self.__line:
314            self.__line = line
315            self.__pointsLayout()
316            self.lineChanged.emit(line)
317
318    def line(self):
319        return self.__line
320
321    def __installFilter(self):
322        for p in self.__points:
323            p.installSceneEventFilter(self)
324
325    def itemChange(self, change, value):
326        if change == QGraphicsItem.ItemSceneHasChanged:
327            if self.scene():
328                self.__installFilter()
329        return QGraphicsObject.itemChange(self, change, value)
330
331    def sceneEventFilter(self, obj, event):
332        try:
333            if isinstance(obj, ControlPoint):
334                etype = event.type()
335                if etype == QEvent.GraphicsSceneMousePress:
336                    self.__setActiveControl(obj)
337                elif etype == QEvent.GraphicsSceneMouseRelease:
338                    self.__setActiveControl(None)
339
340            return QGraphicsObject.sceneEventFilter(self, obj, event)
341        except Exception:
342            log.error("", exc_info=True)
343
344    def __pointsLayout(self):
345        self.__points[0].setPos(self.__line.p1())
346        self.__points[1].setPos(self.__line.p2())
347
348    def __setActiveControl(self, control):
349        if self.__activeControl != control:
350            if self.__activeControl is not None:
351                self.__activeControl.positionChanged[QPointF].disconnect(
352                    self.__activeControlMoved
353                )
354
355            self.__activeControl = control
356
357            if control is not None:
358                control.positionChanged[QPointF].connect(
359                    self.__activeControlMoved
360                )
361
362    def __activeControlMoved(self, pos):
363        line = QLineF(self.__line)
364        control = self.__activeControl
365        if control.anchor() == ControlPoint.TopLeft:
366            line.setP1(pos)
367        elif control.anchor() == ControlPoint.BottomRight:
368            line.setP2(pos)
369
370        if self.__line != line:
371            self.blockSignals(True)
372            self.setLine(line)
373            self.blockSignals(False)
374            self.lineEdited.emit(line)
375
376    def boundingRect(self):
377        return QRectF()
Note: See TracBrowser for help on using the repository browser.