source: orange/Orange/OrangeCanvas/canvas/items/controlpoints.py @ 11274:601a5a575e52

Revision 11274:601a5a575e52, 13.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Emit normalized control rectangles.

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