source: orange/Orange/OrangeCanvas/canvas/items/controlpoints.py @ 11192:d51939aa6f45

Revision 11192:d51939aa6f45, 13.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Changed annotation item selection and (control point) geometry editing.

Control point editing is now fixed to the items focus state.

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        """Return the anchor point at anchor position if not set.
152        """
153        return self.__points.get(anchor)
154
155    def setRect(self, rect):
156        if self.__rect != rect:
157            self.__rect = QRectF(rect)
158            self.__pointsLayout()
159            self.prepareGeometryChange()
160            self.rectChanged.emit(rect)
161
162    def rect(self):
163        """Return the control rect
164        """
165        # Return the rect normalized. During the control point move the
166        # rect can change to an invalid size, but the layout must still
167        # know to which point does an unnormalized rect side belong.
168        return self.__rect.normalized()
169
170    rect_ = Property(QRectF, fget=rect, fset=setRect, user=True)
171
172    def setControlMargins(self, *margins):
173        """Set the controls points on the margins around `rect`
174        """
175        if len(margins) > 1:
176            margins = QMargins(*margins)
177        else:
178            margins = margins[0]
179            if isinstance(margins, int):
180                margins = QMargins(margins, margins, margins, margins)
181
182        if self.__margins != margins:
183            self.__margins = margins
184            self.__pointsLayout()
185
186    def controlMargins(self):
187        return self.__margins
188
189    def setConstraints(self, constraints):
190        pass
191
192    def isControlActive(self):
193        """Return the state of the control. True if the control is
194        active (user is dragging one of the points) False otherwise.
195
196        """
197        return self.__activeControl is not None
198
199    def itemChange(self, change, value):
200        if change == QGraphicsItem.ItemSceneHasChanged and self.scene():
201            self.__installFilter()
202
203        return QGraphicsObject.itemChange(self, change, value)
204
205    def sceneEventFilter(self, obj, event):
206        try:
207            obj = toGraphicsObjectIfPossible(obj)
208            if isinstance(obj, ControlPoint):
209                etype = event.type()
210                if etype == QEvent.GraphicsSceneMousePress and \
211                        event.button() == Qt.LeftButton:
212                    self.__setActiveControl(obj)
213
214                elif etype == QEvent.GraphicsSceneMouseRelease and \
215                        event.button() == Qt.LeftButton:
216                    self.__setActiveControl(None)
217
218        except Exception:
219            log.error("Error in 'ControlPointRect.sceneEventFilter'",
220                      exc_info=True)
221
222        return QGraphicsObject.sceneEventFilter(self, obj, event)
223
224    def __installFilter(self):
225        # Install filters on the control points.
226        try:
227            for p in self.__points.values():
228                p.installSceneEventFilter(self)
229        except Exception:
230            log.error("Error in ControlPointRect.__installFilter",
231                      exc_info=True)
232
233    def __pointsLayout(self):
234        """Layout the control points
235        """
236        rect = self.__rect
237        margins = self.__margins
238        rect = rect.adjusted(-margins.left(), -margins.top(),
239                             margins.right(), margins.bottom())
240        center = rect.center()
241        cx, cy = center.x(), center.y()
242        left, top, right, bottom = \
243                rect.left(), rect.top(), rect.right(), rect.bottom()
244
245        self.controlPoint(ControlPoint.Left).setPos(left, cy)
246        self.controlPoint(ControlPoint.Right).setPos(right, cy)
247        self.controlPoint(ControlPoint.Top).setPos(cx, top)
248        self.controlPoint(ControlPoint.Bottom).setPos(cx, bottom)
249
250        self.controlPoint(ControlPoint.TopLeft).setPos(left, top)
251        self.controlPoint(ControlPoint.TopRight).setPos(right, top)
252        self.controlPoint(ControlPoint.BottomLeft).setPos(left, bottom)
253        self.controlPoint(ControlPoint.BottomRight).setPos(right, bottom)
254
255    def __setActiveControl(self, control):
256        if self.__activeControl != control:
257            if self.__activeControl is not None:
258                self.__activeControl.positionChanged[QPointF].disconnect(
259                    self.__activeControlMoved
260                )
261
262            self.__activeControl = control
263
264            if control is not None:
265                control.positionChanged[QPointF].connect(
266                    self.__activeControlMoved
267                )
268
269    def __activeControlMoved(self, pos):
270        # The active control point has moved, update the control
271        # rectangle
272        control = self.__activeControl
273        pos = control.pos()
274        rect = QRectF(self.__rect)
275        margins = self.__margins
276
277        # TODO: keyboard modifiers and constraints.
278
279        anchor = control.anchor()
280        if anchor & ControlPoint.Top:
281            rect.setTop(pos.y() + margins.top())
282        elif anchor & ControlPoint.Bottom:
283            rect.setBottom(pos.y() - margins.bottom())
284
285        if anchor & ControlPoint.Left:
286            rect.setLeft(pos.x() + margins.left())
287        elif anchor & ControlPoint.Right:
288            rect.setRight(pos.x() - margins.right())
289
290        changed = self.__rect != rect
291
292        self.blockSignals(True)
293        self.setRect(rect)
294        self.blockSignals(False)
295
296        if changed:
297            self.rectEdited.emit(rect)
298
299    def boundingRect(self):
300        return QRectF()
301
302
303class ControlPointLine(QGraphicsObject):
304
305    lineChanged = Signal(QLineF)
306    lineEdited = Signal(QLineF)
307
308    def __init__(self, parent=None, **kwargs):
309        QGraphicsObject.__init__(self, parent, **kwargs)
310        self.setFlag(QGraphicsItem.ItemHasNoContents)
311        self.setFlag(QGraphicsItem.ItemIsFocusable)
312
313        self.__line = QLineF()
314        self.__points = \
315            [ControlPoint(self, ControlPoint.TopLeft),  # TopLeft is line start
316             ControlPoint(self, ControlPoint.BottomRight)  # line end
317             ]
318
319        self.__activeControl = None
320
321        if self.scene():
322            self.__installFilter()
323
324        for p in self.__points:
325            p.setFlag(QGraphicsItem.ItemIsFocusable)
326            p.setFocusProxy(self)
327
328    def setLine(self, line):
329        if not isinstance(line, QLineF):
330            raise TypeError()
331
332        if line != self.__line:
333            self.__line = line
334            self.__pointsLayout()
335            self.lineChanged.emit(line)
336
337    def line(self):
338        return self.__line
339
340    def isControlActive(self):
341        """Return the state of the control. True if the control is
342        active (user is dragging one of the points) False otherwise.
343
344        """
345        return self.__activeControl is not None
346
347    def __installFilter(self):
348        for p in self.__points:
349            p.installSceneEventFilter(self)
350
351    def itemChange(self, change, value):
352        if change == QGraphicsItem.ItemSceneHasChanged:
353            if self.scene():
354                self.__installFilter()
355        return QGraphicsObject.itemChange(self, change, value)
356
357    def sceneEventFilter(self, obj, event):
358        try:
359            obj = toGraphicsObjectIfPossible(obj)
360            if isinstance(obj, ControlPoint):
361                etype = event.type()
362                if etype == QEvent.GraphicsSceneMousePress:
363                    self.__setActiveControl(obj)
364                elif etype == QEvent.GraphicsSceneMouseRelease:
365                    self.__setActiveControl(None)
366
367            return QGraphicsObject.sceneEventFilter(self, obj, event)
368        except Exception:
369            log.error("", exc_info=True)
370
371    def __pointsLayout(self):
372        self.__points[0].setPos(self.__line.p1())
373        self.__points[1].setPos(self.__line.p2())
374
375    def __setActiveControl(self, control):
376        if self.__activeControl != control:
377            if self.__activeControl is not None:
378                self.__activeControl.positionChanged[QPointF].disconnect(
379                    self.__activeControlMoved
380                )
381
382            self.__activeControl = control
383
384            if control is not None:
385                control.positionChanged[QPointF].connect(
386                    self.__activeControlMoved
387                )
388
389    def __activeControlMoved(self, pos):
390        line = QLineF(self.__line)
391        control = self.__activeControl
392        if control.anchor() == ControlPoint.TopLeft:
393            line.setP1(pos)
394        elif control.anchor() == ControlPoint.BottomRight:
395            line.setP2(pos)
396
397        if self.__line != line:
398            self.blockSignals(True)
399            self.setLine(line)
400            self.blockSignals(False)
401            self.lineEdited.emit(line)
402
403    def boundingRect(self):
404        return QRectF()
Note: See TracBrowser for help on using the repository browser.