source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11102:1ae099099c23

Revision 11102:1ae099099c23, 27.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 19 months ago (diff)

Added GraphicsItems representing the items in the workflow scheme.

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
490    def __init__(self, parent=None, **kwargs):
491        Annotation.__init__(self, parent, **kwargs)
492        self.setFlag(QGraphicsItem.ItemIsMovable)
493
494        self.setFocusPolicy(Qt.ClickFocus)
495
496        self.__textMargins = (2, 2, 2, 2)
497
498        rect = self.geometry().translated(-self.pos())
499        self.__framePathItem = QGraphicsPathItem(self)
500        self.__controlPoints = ControlPointRect(self)
501        self.__controlPoints.setRect(rect)
502        self.__controlPoints.rectEdited.connect(self.__onControlRectEdited)
503        self.geometryChanged.connect(self.__updateControlPoints)
504
505        self.__textItem = QGraphicsTextItem(self)
506        self.__textItem.setPos(2, 2)
507        self.__textItem.setTextWidth(rect.width() - 4)
508        self.__textItem.setTabChangesFocus(True)
509        self.__textInteractionFlags = Qt.NoTextInteraction
510
511        layout = self.__textItem.document().documentLayout()
512        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
513
514        self.__updateFrame()
515
516        self.__controlPoints.hide()
517
518    def adjustSize(self):
519        """Resize to a reasonable size.
520        """
521        self.__textItem.setTextWidth(-1)
522        self.__textItem.adjustSize()
523        size = self.__textItem.boundingRect().size()
524        left, top, right, bottom = self.textMargins()
525        geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom))
526        self.setGeometry(geom)
527
528    def setPlainText(self, text):
529        """Set the annotation plain text.
530        """
531        self.__textItem.setPlainText(text)
532
533    def toPlainText(self):
534        return self.__textItem.toPlainText()
535
536    def setHtml(self, text):
537        """Set the annotation rich text.
538        """
539        self.__textItem.setHtml(text)
540
541    def toHtml(self):
542        return self.__textItem.toHtml()
543
544    def setDefaultTextColor(self, color):
545        """Set the default text color.
546        """
547        self.__textItem.setDefaultTextColor(color)
548
549    def defaultTextColor(self):
550        return self.__textItem.defaultTextColor()
551
552    def setTextMargins(self, left, top, right, bottom):
553        """Set the text margins.
554        """
555        margins = (left, top, right, bottom)
556        if self.__textMargins != margins:
557            self.__textMargins = margins
558            self.__textItem.setPos(left, top)
559            self.__textItem.setTextWidth(
560                max(self.geometry().width() - left - right, 0)
561            )
562
563    def textMargins(self):
564        """Return the text margins.
565        """
566        return self.__textMargins
567
568    def document(self):
569        """Return the QTextDocument instance used internally.
570        """
571        return self.__textItem.document()
572
573    def setTextCursor(self, cursor):
574        self.__textItem.setTextCursor(cursor)
575
576    def textCursor(self):
577        return self.__textItem.textCursor()
578
579    def setTextInteractionFlags(self, flags):
580        self.__textInteractionFlags = flags
581        if self.__textItem.hasFocus():
582            self.__textItem.setTextInteractionFlags(flags)
583
584    def textInteractionFlags(self):
585        return self.__textInteractionFlags
586
587    def setDefaultStyleSheet(self, stylesheet):
588        self.document().setDefaultStyleSheet(stylesheet)
589
590    def mouseDoubleClickEvent(self, event):
591        Annotation.mouseDoubleClickEvent(self, event)
592
593        if event.buttons() == Qt.LeftButton and \
594                self.__textInteractionFlags & Qt.TextEditable:
595            self.startEdit()
596
597    def focusInEvent(self, event):
598        # Reparent the control points item to the scene
599        self.__controlPoints.setParentItem(None)
600        self.__controlPoints.show()
601        self.__controlPoints.setZValue(self.zValue() + 3)
602        self.__updateControlPoints()
603        Annotation.focusInEvent(self, event)
604
605    def focusOutEvent(self, event):
606        self.__controlPoints.hide()
607        # Reparent back to self
608        self.__controlPoints.setParentItem(self)
609        Annotation.focusOutEvent(self, event)
610
611    def startEdit(self):
612        """Start the annotation text edit process.
613        """
614        self.__textItem.setTextInteractionFlags(
615                            self.__textInteractionFlags)
616        self.__textItem.setFocus(Qt.MouseFocusReason)
617
618        # Install event filter to find out when the text item loses focus.
619        self.__textItem.installSceneEventFilter(self)
620
621    def endEdit(self):
622        """End the annotation edit.
623        """
624        if self.__textItem.hasFocus():
625            self.__textItem.clearFocus()
626
627        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
628        self.__textItem.removeSceneEventFilter(self)
629
630    def __onDocumentSizeChanged(self, size):
631        # The size of the text document has changed. Expand the text
632        # control rect's height if the text no longer fits inside.
633        try:
634            rect = self.geometry()
635            _, top, _, bottom = self.textMargins()
636            if rect.height() < (size.height() + bottom + top):
637                rect.setHeight(size.height() + bottom + top)
638                self.setGeometry(rect)
639        except Exception:
640            log.error("error in __onDocumentSizeChanged",
641                      exc_info=True)
642
643    def __onControlRectEdited(self, newrect):
644        # The control rect has been edited by the user
645        # new rect is ins scene coordinates
646        try:
647            newpos = newrect.topLeft()
648            parent = self.parentItem()
649            if parent:
650                newpos = parent.mapFromScene(newpos)
651
652            geom = QRectF(newpos, newrect.size())
653            self.setGeometry(geom)
654        except Exception:
655            log.error("An error occurred in '__onControlRectEdited'",
656                      exc_info=True)
657
658    def __updateFrame(self):
659        rect = self.geometry()
660        rect.moveTo(0, 0)
661        path = QPainterPath()
662        path.addRect(rect)
663        self.__framePathItem.setPath(path)
664
665    def __updateControlPoints(self, *args):
666        """Update the control points geometry.
667        """
668        if not self.__controlPoints.isVisible():
669            return
670
671        try:
672            geom = self.geometry()
673            parent = self.parentItem()
674            # The control rect is in scene coordinates
675            if parent is not None:
676                geom = QRectF(parent.mapToScene(geom.topLeft()),
677                              geom.size())
678            self.__controlPoints.setRect(geom)
679        except Exception:
680            log.error("An error occurred in '__updateControlPoints'",
681                      exc_info=True)
682
683    def resizeEvent(self, event):
684        width = event.newSize().width()
685        left, _, right, _ = self.textMargins()
686        self.__textItem.setTextWidth(max(width - left - right, 0))
687        self.__updateFrame()
688        self.__updateControlPoints()
689        QGraphicsWidget.resizeEvent(self, event)
690
691    def sceneEventFilter(self, obj, event):
692        if obj is self.__textItem and event.type() == QEvent.FocusOut:
693            self.__textItem.focusOutEvent(event)
694            self.endEdit()
695            return True
696
697        return Annotation.sceneEventFilter(self, obj, event)
698
699
700class ArrowItem(GraphicsPathObject):
701    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
702        GraphicsPathObject.__init__(self, parent, **kwargs)
703
704        if line is None:
705            line = QLineF(0, 0, 10, 0)
706
707        self.__line = line
708
709        self.__lineWidth = lineWidth
710
711        self.__updateArrowPath()
712
713    def setLine(self, line):
714        if self.__line != line:
715            self.__line = line
716            self.__updateArrowPath()
717
718    def line(self):
719        return self.__line
720
721    def setLineWidth(self, lineWidth):
722        if self.__lineWidth != lineWidth:
723            self.__lineWidth = lineWidth
724            self.__updateArrowPath()
725
726    def lineWidth(self):
727        return self.__lineWidth
728
729    def __updateArrowPath(self):
730        line = self.__line
731        width = self.__lineWidth
732        path = QPainterPath()
733        p1, p2 = line.p1(), line.p2()
734        if p1 == p2:
735            self.setPath(path)
736            return
737
738        baseline = QLineF(line)
739        baseline.setLength(max(line.length() - width * 3, width * 3))
740        path.moveTo(baseline.p1())
741        path.lineTo(baseline.p2())
742
743        stroker = QPainterPathStroker()
744        stroker.setWidth(width)
745        path = stroker.createStroke(path)
746
747        arrow_head_len = width * 4
748        arrow_head_angle = 60
749        line_angle = line.angle() - 180
750
751        angle_1 = line_angle - arrow_head_angle / 2.0
752        angle_2 = line_angle + arrow_head_angle / 2.0
753
754        points = [p2,
755                  p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
756                  p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
757                  p2]
758        poly = QPolygonF(points)
759        path_head = QPainterPath()
760        path_head.addPolygon(poly)
761        path = path.united(path_head)
762        self.setPath(path)
763
764
765class ArrowAnnotation(Annotation):
766    def __init__(self, parent=None, line=None, **kwargs):
767        Annotation.__init__(self, parent, **kwargs)
768        self.setFlag(QGraphicsItem.ItemIsMovable)
769        self.setFocusPolicy(Qt.ClickFocus)
770
771        if line is None:
772            line = QLineF(0, 0, 20, 0)
773
774        self.__line = line
775        self.__arrowItem = ArrowItem(self)
776        self.__arrowItem.setLine(line)
777        self.__arrowItem.setBrush(Qt.red)
778        self.__arrowItem.setPen(Qt.NoPen)
779        self.__controlPointLine = ControlPointLine(self)
780        self.__controlPointLine.setLine(line)
781        self.__controlPointLine.hide()
782        self.__controlPointLine.lineEdited.connect(self.__onLineEdited)
783
784    def setLine(self, line):
785        """Set the arrow base line (a QLineF in object coordinates).
786        """
787        if self.__line != line:
788            self.__line = line
789#            self.__arrowItem.setLine(line)
790            # Check if the line does not fit inside the geometry.
791
792            geom = self.geometry().translated(-self.pos())
793
794            if geom.isNull() and not line.isNull():
795                geom = QRectF(0, 0, 1, 1)
796            line_rect = QRectF(line.p1(), line.p2())
797
798            if not (geom.contains(line_rect)):
799                geom = geom.united(line_rect)
800
801            diff = geom.topLeft()
802            line = QLineF(line.p1() - diff, line.p2() - diff)
803            self.__arrowItem.setLine(line)
804            self.__line = line
805
806            geom.translate(self.pos())
807            self.setGeometry(geom)
808
809    def adjustGeometry(self):
810        """Adjust the widget geometry to exactly fit the arrow inside
811        preserving the arrow path scene geometry.
812
813        """
814        geom = self.geometry().translated(-self.pos())
815        line = self.__line
816        line_rect = QRectF(line.p1(), line.p2()).normalized()
817        if geom.isNull() and not line.isNull():
818            geom = QRectF(0, 0, 1, 1)
819        if not (geom.contains(line_rect)):
820            geom = geom.united(line_rect)
821        geom = geom.intersected(line_rect)
822        diff = geom.topLeft()
823        line = QLineF(line.p1() - diff, line.p2() - diff)
824        geom.translate(self.pos())
825        self.setGeometry(geom)
826        self.setLine(line)
827
828    def line(self):
829        return self.__line
830
831    def setLineWidth(self, lineWidth):
832        self.__arrowItem.setLineWidth(lineWidth)
833
834    def lineWidth(self):
835        return self.__arrowItem.lineWidth()
836
837    def focusInEvent(self, event):
838        self.__controlPointLine.setParentItem(None)
839        self.__controlPointLine.show()
840        self.__controlPointLine.setZValue(self.zValue() + 3)
841        self.__updateControlLine()
842        self.geometryChanged.connect(self.__onGeometryChange)
843        return Annotation.focusInEvent(self, event)
844
845    def focusOutEvent(self, event):
846        self.__controlPointLine.hide()
847        self.__controlPointLine.setParentItem(self)
848        self.geometryChanged.disconnect(self.__onGeometryChange)
849        return Annotation.focusOutEvent(self, event)
850
851    def __updateControlLine(self):
852        if not self.__controlPointLine.isVisible():
853            return
854
855        line = self.__line
856        line = QLineF(self.mapToScene(line.p1()),
857                      self.mapToScene(line.p2()))
858        self.__controlPointLine.setLine(line)
859
860    def __onLineEdited(self, line):
861        line = QLineF(self.mapFromScene(line.p1()),
862                      self.mapFromScene(line.p2()))
863        self.setLine(line)
864
865    def __onGeometryChange(self):
866        if self.__controlPointLine.isVisible():
867            self.__updateControlLine()
868
869    def shape(self):
870        arrow_shape = self.__arrowItem.shape()
871        return self.mapFromItem(self.__arrowItem, arrow_shape)
872
873#    def paint(self, painter, option, widget=None):
874#        painter.drawRect(self.geometry().translated(-self.pos()))
875#        return Annotation.paint(self, painter, option, widget)
Note: See TracBrowser for help on using the repository browser.