source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11151:e1bfd808981c

Revision 11151:e1bfd808981c, 27.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Refactored user interactions (and implementing undo/redo).

Only annotation control point editing does not yet have undo/redo.

Line 
1
2import logging
3
4from PyQt4.QtGui import (
5    QGraphicsItem, QGraphicsObject, QGraphicsPathItem, QGraphicsWidget,
6    QGraphicsTextItem, QPainterPath, QPainterPathStroker,
7    QPen, QBrush, QPolygonF
8)
9
10from PyQt4.QtCore import (
11    Qt, QPointF, QSizeF, QRectF, QLineF, QMargins, QEvent, QVariant
12)
13
14from PyQt4.QtCore import pyqtSignal as Signal, pyqtProperty as Property
15
16log = logging.getLogger(__name__)
17
18
19class GraphicsPathObject(QGraphicsObject):
20    """A QGraphicsObject subclass implementing an interface similar to
21    QGraphicsPathItem.
22
23    """
24    def __init__(self, parent=None, **kwargs):
25        QGraphicsObject.__init__(self, parent, **kwargs)
26
27        self.__boundingRect = None
28        self.__path = QPainterPath()
29        self.__brush = QBrush(Qt.NoBrush)
30        self.__pen = QPen()
31
32    def setPath(self, path):
33        """Set the path shape for the object point.
34        """
35        if not isinstance(path, QPainterPath):
36            raise TypeError("%r, 'QPainterPath' expected" % type(path))
37
38        if self.__path != path:
39            self.prepareGeometryChange()
40            self.__path = path
41            self.__boundingRect = None
42            self.update()
43
44    def path(self):
45        return self.__path
46
47    def setBrush(self, brush):
48        if not isinstance(brush, QBrush):
49            brush = QBrush(brush)
50
51        if self.__brush != brush:
52            self.__brush = brush
53            self.update()
54
55    def brush(self):
56        return self.__brush
57
58    def setPen(self, pen):
59        if not isinstance(pen, QPen):
60            pen = QPen(pen)
61
62        if self.__pen != pen:
63            self.prepareGeometryChange()
64            self.__pen = pen
65            self.__boundingRect = None
66            self.update()
67
68    def pen(self):
69        return self.__pen
70
71    def paint(self, painter, option, widget=None):
72        if self.__path.isEmpty():
73            return
74
75        painter.save()
76        painter.setPen(self.pen())
77        painter.setBrush(self.brush())
78        painter.drawPath(self.path())
79        painter.restore()
80
81    def boundingRect(self):
82        if self.__boundingRect is None:
83            br = self.__path.controlPointRect()
84            pen_w = self.__pen.widthF()
85            self.__boundingRect = br.adjusted(-pen_w, -pen_w, pen_w, pen_w)
86
87        return self.__boundingRect
88
89    def shape(self):
90        return shapeForPath(self.__path, self.__pen)
91
92
93def shapeForPath(path, pen):
94    """Create a QPainterPath shape from the path drawn with pen.
95    """
96    stroker = QPainterPathStroker()
97    stroker.setWidth(max(pen.width(), 1))
98    shape = stroker.createStroke(path)
99    shape.addPath(path)
100    return shape
101
102
103class ControlPoint(GraphicsPathObject):
104    """A control point for annotations in the canvas.
105    """
106    Free = 0
107
108    Left, Top, Right, Bottom, Center = 1, 2, 4, 8, 16
109
110    TopLeft = Top | Left
111    TopRight = Top | Right
112    BottomRight = Bottom | Right
113    BottomLeft = Bottom | Left
114
115    posChanged = Signal(QPointF)
116
117    def __init__(self, parent=None, anchor=0, **kwargs):
118        GraphicsPathObject.__init__(self, parent, **kwargs)
119        self.setFlag(QGraphicsItem.ItemIsMovable)
120        self.setAcceptedMouseButtons(Qt.LeftButton)
121
122        self.__posEmitted = self.pos()  # Last emitted position
123        self.xChanged.connect(self.__emitPosChanged)
124        self.yChanged.connect(self.__emitPosChanged)
125
126        self.__constraint = 0
127        self.__constraintFunc = None
128        self.__anchor = 0
129        self.setAnchor(anchor)
130
131        path = QPainterPath()
132        path.addEllipse(QRectF(-4, -4, 8, 8))
133        self.setPath(path)
134
135        self.setBrush(QBrush(Qt.lightGray, Qt.SolidPattern))
136
137    def setAnchor(self, anchor):
138        """Set anchor position
139        """
140        self.__anchor = anchor
141
142    def anchor(self):
143        return self.__anchor
144
145    def mousePressEvent(self, event):
146        if event.button() == Qt.LeftButton:
147            # Enable ItemPositionChange (and pos constraint) only when
148            # this is the mouse grabber item
149            self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
150        return GraphicsPathObject.mousePressEvent(self, event)
151
152    def mouseReleaseEvent(self, event):
153        if event.button() == Qt.LeftButton:
154            self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, False)
155        return GraphicsPathObject.mouseReleaseEvent(self, event)
156
157    def itemChange(self, change, value):
158
159        if change == QGraphicsItem.ItemPositionChange:
160            pos = value.toPointF()
161            newpos = self.constrain(pos)
162            return QVariant(newpos)
163
164        return GraphicsPathObject.itemChange(self, change, value)
165
166    def __emitPosChanged(self, *args):
167        # Emit the posChanged signal if the current pos is different
168        # from the last emitted pos
169        pos = self.pos()
170        if pos != self.__posEmitted:
171            self.posChanged.emit(pos)
172            self.__posEmitted = pos
173
174    def hasConstraint(self):
175        return self.__constraintFunc is not None or self.__constraint != 0
176
177    def setConstraint(self, constraint):
178        """Set the constraint for the point (Qt.Vertical Qt.Horizontal or 0)
179
180        .. note:: Clears the constraintFunc if it was previously set
181
182        """
183        if self.__constraint != constraint:
184            self.__constraint = constraint
185
186        self.__constraintFunc = None
187
188    def constrain(self, pos):
189        """Constrain the pos.
190        """
191        if self.__constraintFunc:
192            return self.__constraintFunc(pos)
193        elif self.__constraint == Qt.Vertical:
194            return QPointF(self.pos().x(), pos.y())
195        elif self.__constraint == Qt.Horizontal:
196            return QPointF(pos.x(), self.pos().y())
197        else:
198            return pos
199
200    def setConstraintFunc(self, func):
201        if self.__constraintFunc != func:
202            self.__constraintFunc = func
203
204
205class ControlPointRect(QGraphicsObject):
206    Free = 0
207    KeepAspectRatio = 1
208    KeepCenter = 2
209
210    rectChanged = Signal(QRectF)
211    rectEdited = Signal(QRectF)
212
213    def __init__(self, parent=None, rect=None, constraints=0, **kwargs):
214        QGraphicsObject.__init__(self, parent, **kwargs)
215        self.setFlag(QGraphicsItem.ItemHasNoContents)
216
217        self.__rect = rect if rect is not None else QRectF()
218        self.__margins = QMargins()
219        points = \
220            [ControlPoint(self, ControlPoint.Left),
221             ControlPoint(self, ControlPoint.Top),
222             ControlPoint(self, ControlPoint.TopLeft),
223             ControlPoint(self, ControlPoint.Right),
224             ControlPoint(self, ControlPoint.TopRight),
225             ControlPoint(self, ControlPoint.Bottom),
226             ControlPoint(self, ControlPoint.BottomLeft),
227             ControlPoint(self, ControlPoint.BottomRight)
228             ]
229        assert(points == sorted(points, key=lambda p: p.anchor()))
230
231        self.__points = dict((p.anchor(), p) for p in points)
232
233        if self.scene():
234            self.__installFilter()
235
236        self.controlPoint(ControlPoint.Top).setConstraint(Qt.Vertical)
237        self.controlPoint(ControlPoint.Bottom).setConstraint(Qt.Vertical)
238        self.controlPoint(ControlPoint.Left).setConstraint(Qt.Horizontal)
239        self.controlPoint(ControlPoint.Right).setConstraint(Qt.Horizontal)
240
241        self.__constraints = constraints
242        self.__activeControl = None
243
244        self.__pointsLayout()
245
246    def controlPoint(self, anchor):
247        """Return the anchor point at anchor position if not set.
248        """
249        return self.__points.get(anchor)
250
251    def setRect(self, rect):
252        if self.__rect != rect:
253            self.__rect = rect
254            self.__pointsLayout()
255            self.prepareGeometryChange()
256            self.rectChanged.emit(rect)
257
258    def rect(self):
259        """Return the control rect
260        """
261        # Return the rect normalized. During the control point move the
262        # rect can change to an invalid size, but the layout must still
263        # know to which point does an unnormalized rect side belong.
264        return self.__rect.normalized()
265
266    rect_ = Property(QRectF, fget=rect, fset=setRect, user=True)
267
268    def setControlMargins(self, *margins):
269        """Set the controls points on the margins around `rect`
270        """
271        if len(margins) > 1:
272            margins = QMargins(*margins)
273        else:
274            margins = margins[0]
275            if isinstance(margins, int):
276                margins = QMargins(margins, margins, margins, margins)
277
278        if self.__margins != margins:
279            self.__margins = margins
280            self.__pointsLayout()
281
282    def controlMargins(self):
283        return self.__margins
284
285    def setConstraints(self, constraints):
286        pass
287
288    def itemChange(self, change, value):
289        if change == QGraphicsItem.ItemSceneHasChanged and self.scene():
290            self.__installFilter()
291
292        return QGraphicsObject.itemChange(self, change, value)
293
294    def sceneEventFilter(self, obj, event):
295        try:
296            if isinstance(obj, ControlPoint):
297                etype = event.type()
298                if etype == QEvent.GraphicsSceneMousePress and \
299                        event.button() == Qt.LeftButton:
300                    self.__setActiveControl(obj)
301
302                elif etype == QEvent.GraphicsSceneMouseRelease and \
303                        event.button() == Qt.LeftButton:
304                    self.__setActiveControl(None)
305
306        except Exception:
307            log.error("Error in 'ControlPointRect.sceneEventFilter'",
308                      exc_info=True)
309
310        return QGraphicsObject.sceneEventFilter(self, obj, event)
311
312    def __installFilter(self):
313        # Install filters on the control points.
314        try:
315            for p in self.__points.values():
316                p.installSceneEventFilter(self)
317        except Exception:
318            log.error("Error in ControlPointRect.__installFilter",
319                      exc_info=True)
320
321    def __pointsLayout(self):
322        """Layout the control points
323        """
324        rect = self.__rect
325        margins = self.__margins
326        rect = rect.adjusted(-margins.left(), -margins.top(),
327                             margins.right(), margins.bottom())
328        center = rect.center()
329        cx, cy = center.x(), center.y()
330        left, top, right, bottom = \
331                rect.left(), rect.top(), rect.right(), rect.bottom()
332
333        self.controlPoint(ControlPoint.Left).setPos(left, cy)
334        self.controlPoint(ControlPoint.Right).setPos(right, cy)
335        self.controlPoint(ControlPoint.Top).setPos(cx, top)
336        self.controlPoint(ControlPoint.Bottom).setPos(cx, bottom)
337
338        self.controlPoint(ControlPoint.TopLeft).setPos(left, top)
339        self.controlPoint(ControlPoint.TopRight).setPos(right, top)
340        self.controlPoint(ControlPoint.BottomLeft).setPos(left, bottom)
341        self.controlPoint(ControlPoint.BottomRight).setPos(right, bottom)
342
343    def __setActiveControl(self, control):
344        if self.__activeControl != control:
345            if self.__activeControl is not None:
346                self.__activeControl.posChanged.disconnect(
347                    self.__activeControlMoved
348                )
349
350            self.__activeControl = control
351
352            if control is not None:
353                control.posChanged.connect(self.__activeControlMoved)
354
355    def __activeControlMoved(self, pos):
356        # The active control point has moved, update the control
357        # rectangle
358        control = self.__activeControl
359        pos = control.pos()
360        rect = QRectF(self.__rect)
361        margins = self.__margins
362
363        # TODO: keyboard modifiers and constraints.
364
365        anchor = control.anchor()
366        if anchor & ControlPoint.Top:
367            rect.setTop(pos.y() + margins.top())
368        elif anchor & ControlPoint.Bottom:
369            rect.setBottom(pos.y() - margins.bottom())
370
371        if anchor & ControlPoint.Left:
372            rect.setLeft(pos.x() + margins.left())
373        elif anchor & ControlPoint.Right:
374            rect.setRight(pos.x() - margins.right())
375
376        changed = self.__rect != rect
377
378        self.blockSignals(True)
379        self.setRect(rect)
380        self.blockSignals(False)
381
382        if changed:
383            self.rectEdited.emit(rect)
384
385    def boundingRect(self):
386        return QRectF()
387
388
389class ControlPointLine(QGraphicsObject):
390
391    lineChanged = Signal(QLineF)
392    lineEdited = Signal(QLineF)
393
394    def __init__(self, parent=None, **kwargs):
395        QGraphicsObject.__init__(self, parent, **kwargs)
396        self.setFlag(QGraphicsItem.ItemHasNoContents)
397
398        self.__line = QLineF()
399        self.__points = \
400            [ControlPoint(self, ControlPoint.TopLeft),  # TopLeft is line start
401             ControlPoint(self, ControlPoint.BottomRight)  # line end
402             ]
403
404        self.__activeControl = None
405
406        if self.scene():
407            self.__installFilter()
408
409    def setLine(self, line):
410        if not isinstance(line, QLineF):
411            raise TypeError()
412
413        if line != self.__line:
414            self.__line = line
415            self.__pointsLayout()
416            self.lineChanged.emit(line)
417
418    def line(self):
419        return self.__line
420
421    def __installFilter(self):
422        for p in self.__points:
423            p.installSceneEventFilter(self)
424
425    def itemChange(self, change, value):
426        if change == QGraphicsItem.ItemSceneHasChanged:
427            if self.scene():
428                self.__installFilter()
429        return QGraphicsObject.itemChange(self, change, value)
430
431    def sceneEventFilter(self, obj, event):
432        try:
433            if isinstance(obj, ControlPoint):
434                etype = event.type()
435                if etype == QEvent.GraphicsSceneMousePress:
436                    self.__setActiveControl(obj)
437                elif etype == QEvent.GraphicsSceneMouseRelease:
438                    self.__setActiveControl(None)
439
440            return QGraphicsObject.sceneEventFilter(self, obj, event)
441        except Exception:
442            log.error("", exc_info=True)
443
444    def __pointsLayout(self):
445        self.__points[0].setPos(self.__line.p1())
446        self.__points[1].setPos(self.__line.p2())
447
448    def __setActiveControl(self, control):
449        if self.__activeControl != control:
450            if self.__activeControl is not None:
451                self.__activeControl.posChanged.disconnect(
452                    self.__activeControlMoved
453                )
454
455            self.__activeControl = control
456
457            if control is not None:
458                control.posChanged.connect(self.__activeControlMoved)
459
460    def __activeControlMoved(self, pos):
461        line = QLineF(self.__line)
462        control = self.__activeControl
463        if control.anchor() == ControlPoint.TopLeft:
464            line.setP1(pos)
465        elif control.anchor() == ControlPoint.BottomRight:
466            line.setP2(pos)
467
468        if self.__line != line:
469            self.blockSignals(True)
470            self.setLine(line)
471            self.blockSignals(False)
472            self.lineEdited.emit(line)
473
474    def boundingRect(self):
475        return QRectF()
476
477
478class Annotation(QGraphicsWidget):
479    """Base class for annotations in the canvas scheme.
480    """
481    def __init__(self, parent=None, **kwargs):
482        QGraphicsWidget.__init__(self, parent, **kwargs)
483
484
485class TextAnnotation(Annotation):
486    """Text annotation for the canvas scheme.
487
488    """
489    editingFinished = Signal()
490    textEdited = Signal()
491
492    def __init__(self, parent=None, **kwargs):
493        Annotation.__init__(self, parent, **kwargs)
494        self.setFlag(QGraphicsItem.ItemIsMovable)
495
496        self.setFocusPolicy(Qt.ClickFocus)
497
498        self.__textMargins = (2, 2, 2, 2)
499
500        rect = self.geometry().translated(-self.pos())
501        self.__framePathItem = QGraphicsPathItem(self)
502        self.__controlPoints = ControlPointRect(self)
503        self.__controlPoints.setRect(rect)
504        self.__controlPoints.rectEdited.connect(self.__onControlRectEdited)
505        self.geometryChanged.connect(self.__updateControlPoints)
506
507        self.__textItem = QGraphicsTextItem(self)
508        self.__textItem.setPos(2, 2)
509        self.__textItem.setTextWidth(rect.width() - 4)
510        self.__textItem.setTabChangesFocus(True)
511        self.__textInteractionFlags = Qt.NoTextInteraction
512
513        layout = self.__textItem.document().documentLayout()
514        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
515
516        self.__updateFrame()
517
518        self.__controlPoints.hide()
519
520    def adjustSize(self):
521        """Resize to a reasonable size.
522        """
523        self.__textItem.setTextWidth(-1)
524        self.__textItem.adjustSize()
525        size = self.__textItem.boundingRect().size()
526        left, top, right, bottom = self.textMargins()
527        geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom))
528        self.setGeometry(geom)
529
530    def setPlainText(self, text):
531        """Set the annotation plain text.
532        """
533        self.__textItem.setPlainText(text)
534
535    def toPlainText(self):
536        return self.__textItem.toPlainText()
537
538    def setHtml(self, text):
539        """Set the annotation rich text.
540        """
541        self.__textItem.setHtml(text)
542
543    def toHtml(self):
544        return self.__textItem.toHtml()
545
546    def setDefaultTextColor(self, color):
547        """Set the default text color.
548        """
549        self.__textItem.setDefaultTextColor(color)
550
551    def defaultTextColor(self):
552        return self.__textItem.defaultTextColor()
553
554    def setTextMargins(self, left, top, right, bottom):
555        """Set the text margins.
556        """
557        margins = (left, top, right, bottom)
558        if self.__textMargins != margins:
559            self.__textMargins = margins
560            self.__textItem.setPos(left, top)
561            self.__textItem.setTextWidth(
562                max(self.geometry().width() - left - right, 0)
563            )
564
565    def textMargins(self):
566        """Return the text margins.
567        """
568        return self.__textMargins
569
570    def document(self):
571        """Return the QTextDocument instance used internally.
572        """
573        return self.__textItem.document()
574
575    def setTextCursor(self, cursor):
576        self.__textItem.setTextCursor(cursor)
577
578    def textCursor(self):
579        return self.__textItem.textCursor()
580
581    def setTextInteractionFlags(self, flags):
582        self.__textInteractionFlags = flags
583        if self.__textItem.hasFocus():
584            self.__textItem.setTextInteractionFlags(flags)
585
586    def textInteractionFlags(self):
587        return self.__textInteractionFlags
588
589    def setDefaultStyleSheet(self, stylesheet):
590        self.document().setDefaultStyleSheet(stylesheet)
591
592    def mouseDoubleClickEvent(self, event):
593        Annotation.mouseDoubleClickEvent(self, event)
594
595        if event.buttons() == Qt.LeftButton and \
596                self.__textInteractionFlags & Qt.TextEditable:
597            self.startEdit()
598
599    def focusInEvent(self, event):
600        # Reparent the control points item to the scene
601        self.__controlPoints.setParentItem(None)
602        self.__controlPoints.show()
603        self.__controlPoints.setZValue(self.zValue() + 3)
604        self.__updateControlPoints()
605        Annotation.focusInEvent(self, event)
606
607    def focusOutEvent(self, event):
608        self.__controlPoints.hide()
609        # Reparent back to self
610        self.__controlPoints.setParentItem(self)
611        Annotation.focusOutEvent(self, event)
612
613    def startEdit(self):
614        """Start the annotation text edit process.
615        """
616        self.__textItem.setTextInteractionFlags(
617                            self.__textInteractionFlags)
618        self.__textItem.setFocus(Qt.MouseFocusReason)
619
620        # Install event filter to find out when the text item loses focus.
621        self.__textItem.installSceneEventFilter(self)
622        self.__textItem.document().contentsChanged.connect(
623            self.textEdited
624        )
625
626    def endEdit(self):
627        """End the annotation edit.
628        """
629        if self.__textItem.hasFocus():
630            self.__textItem.clearFocus()
631
632        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
633        self.__textItem.removeSceneEventFilter(self)
634        self.__textItem.document().contentsChanged.disconnect(
635            self.textEdited
636        )
637        self.editingFinished.emit()
638
639    def __onDocumentSizeChanged(self, size):
640        # The size of the text document has changed. Expand the text
641        # control rect's height if the text no longer fits inside.
642        try:
643            rect = self.geometry()
644            _, top, _, bottom = self.textMargins()
645            if rect.height() < (size.height() + bottom + top):
646                rect.setHeight(size.height() + bottom + top)
647                self.setGeometry(rect)
648        except Exception:
649            log.error("error in __onDocumentSizeChanged",
650                      exc_info=True)
651
652    def __onControlRectEdited(self, newrect):
653        # The control rect has been edited by the user
654        # new rect is ins scene coordinates
655        try:
656            newpos = newrect.topLeft()
657            parent = self.parentItem()
658            if parent:
659                newpos = parent.mapFromScene(newpos)
660
661            geom = QRectF(newpos, newrect.size())
662            self.setGeometry(geom)
663        except Exception:
664            log.error("An error occurred in '__onControlRectEdited'",
665                      exc_info=True)
666
667    def __updateFrame(self):
668        rect = self.geometry()
669        rect.moveTo(0, 0)
670        path = QPainterPath()
671        path.addRect(rect)
672        self.__framePathItem.setPath(path)
673
674    def __updateControlPoints(self, *args):
675        """Update the control points geometry.
676        """
677        if not self.__controlPoints.isVisible():
678            return
679
680        try:
681            geom = self.geometry()
682            parent = self.parentItem()
683            # The control rect is in scene coordinates
684            if parent is not None:
685                geom = QRectF(parent.mapToScene(geom.topLeft()),
686                              geom.size())
687            self.__controlPoints.setRect(geom)
688        except Exception:
689            log.error("An error occurred in '__updateControlPoints'",
690                      exc_info=True)
691
692    def resizeEvent(self, event):
693        width = event.newSize().width()
694        left, _, right, _ = self.textMargins()
695        self.__textItem.setTextWidth(max(width - left - right, 0))
696        self.__updateFrame()
697        self.__updateControlPoints()
698        QGraphicsWidget.resizeEvent(self, event)
699
700    def sceneEventFilter(self, obj, event):
701        if obj is self.__textItem and event.type() == QEvent.FocusOut:
702            self.__textItem.focusOutEvent(event)
703            self.endEdit()
704            return True
705
706        return Annotation.sceneEventFilter(self, obj, event)
707
708
709class ArrowItem(GraphicsPathObject):
710    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
711        GraphicsPathObject.__init__(self, parent, **kwargs)
712
713        if line is None:
714            line = QLineF(0, 0, 10, 0)
715
716        self.__line = line
717
718        self.__lineWidth = lineWidth
719
720        self.__updateArrowPath()
721
722    def setLine(self, line):
723        if self.__line != line:
724            self.__line = line
725            self.__updateArrowPath()
726
727    def line(self):
728        return self.__line
729
730    def setLineWidth(self, lineWidth):
731        if self.__lineWidth != lineWidth:
732            self.__lineWidth = lineWidth
733            self.__updateArrowPath()
734
735    def lineWidth(self):
736        return self.__lineWidth
737
738    def __updateArrowPath(self):
739        line = self.__line
740        width = self.__lineWidth
741        path = QPainterPath()
742        p1, p2 = line.p1(), line.p2()
743        if p1 == p2:
744            self.setPath(path)
745            return
746
747        baseline = QLineF(line)
748        baseline.setLength(max(line.length() - width * 3, width * 3))
749        path.moveTo(baseline.p1())
750        path.lineTo(baseline.p2())
751
752        stroker = QPainterPathStroker()
753        stroker.setWidth(width)
754        path = stroker.createStroke(path)
755
756        arrow_head_len = width * 4
757        arrow_head_angle = 60
758        line_angle = line.angle() - 180
759
760        angle_1 = line_angle - arrow_head_angle / 2.0
761        angle_2 = line_angle + arrow_head_angle / 2.0
762
763        points = [p2,
764                  p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
765                  p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
766                  p2]
767        poly = QPolygonF(points)
768        path_head = QPainterPath()
769        path_head.addPolygon(poly)
770        path = path.united(path_head)
771        self.setPath(path)
772
773
774class ArrowAnnotation(Annotation):
775    def __init__(self, parent=None, line=None, **kwargs):
776        Annotation.__init__(self, parent, **kwargs)
777        self.setFlag(QGraphicsItem.ItemIsMovable)
778        self.setFocusPolicy(Qt.ClickFocus)
779
780        if line is None:
781            line = QLineF(0, 0, 20, 0)
782
783        self.__line = line
784        self.__arrowItem = ArrowItem(self)
785        self.__arrowItem.setLine(line)
786        self.__arrowItem.setBrush(Qt.red)
787        self.__arrowItem.setPen(Qt.NoPen)
788        self.__controlPointLine = ControlPointLine(self)
789        self.__controlPointLine.setLine(line)
790        self.__controlPointLine.hide()
791        self.__controlPointLine.lineEdited.connect(self.__onLineEdited)
792
793    def setLine(self, line):
794        """Set the arrow base line (a QLineF in object coordinates).
795        """
796        if self.__line != line:
797            self.__line = line
798#            self.__arrowItem.setLine(line)
799            # Check if the line does not fit inside the geometry.
800
801            geom = self.geometry().translated(-self.pos())
802
803            if geom.isNull() and not line.isNull():
804                geom = QRectF(0, 0, 1, 1)
805            line_rect = QRectF(line.p1(), line.p2())
806
807            if not (geom.contains(line_rect)):
808                geom = geom.united(line_rect)
809
810            diff = geom.topLeft()
811            line = QLineF(line.p1() - diff, line.p2() - diff)
812            self.__arrowItem.setLine(line)
813            self.__line = line
814
815            geom.translate(self.pos())
816            self.setGeometry(geom)
817
818    def adjustGeometry(self):
819        """Adjust the widget geometry to exactly fit the arrow inside
820        preserving the arrow path scene geometry.
821
822        """
823        geom = self.geometry().translated(-self.pos())
824        line = self.__line
825        line_rect = QRectF(line.p1(), line.p2()).normalized()
826        if geom.isNull() and not line.isNull():
827            geom = QRectF(0, 0, 1, 1)
828        if not (geom.contains(line_rect)):
829            geom = geom.united(line_rect)
830        geom = geom.intersected(line_rect)
831        diff = geom.topLeft()
832        line = QLineF(line.p1() - diff, line.p2() - diff)
833        geom.translate(self.pos())
834        self.setGeometry(geom)
835        self.setLine(line)
836
837    def line(self):
838        return self.__line
839
840    def setLineWidth(self, lineWidth):
841        self.__arrowItem.setLineWidth(lineWidth)
842
843    def lineWidth(self):
844        return self.__arrowItem.lineWidth()
845
846    def focusInEvent(self, event):
847        self.__controlPointLine.setParentItem(None)
848        self.__controlPointLine.show()
849        self.__controlPointLine.setZValue(self.zValue() + 3)
850        self.__updateControlLine()
851        self.geometryChanged.connect(self.__onGeometryChange)
852        return Annotation.focusInEvent(self, event)
853
854    def focusOutEvent(self, event):
855        self.__controlPointLine.hide()
856        self.__controlPointLine.setParentItem(self)
857        self.geometryChanged.disconnect(self.__onGeometryChange)
858        return Annotation.focusOutEvent(self, event)
859
860    def __updateControlLine(self):
861        if not self.__controlPointLine.isVisible():
862            return
863
864        line = self.__line
865        line = QLineF(self.mapToScene(line.p1()),
866                      self.mapToScene(line.p2()))
867        self.__controlPointLine.setLine(line)
868
869    def __onLineEdited(self, line):
870        line = QLineF(self.mapFromScene(line.p1()),
871                      self.mapFromScene(line.p2()))
872        self.setLine(line)
873
874    def __onGeometryChange(self):
875        if self.__controlPointLine.isVisible():
876            self.__updateControlLine()
877
878    def shape(self):
879        arrow_shape = self.__arrowItem.shape()
880        return self.mapFromItem(self.__arrowItem, arrow_shape)
881
882#    def paint(self, painter, option, widget=None):
883#        painter.drawRect(self.geometry().translated(-self.pos()))
884#        return Annotation.paint(self, painter, option, widget)
Note: See TracBrowser for help on using the repository browser.