source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11401:c12e0617fe38

Revision 11401:c12e0617fe38, 17.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Added 'autoAdjustGeometry' option to ArrowAnnotation class.

RevLine 
[11102]1
2import logging
3
4from PyQt4.QtGui import (
[11185]5    QGraphicsItem, QGraphicsPathItem, QGraphicsWidget, QGraphicsTextItem,
6    QGraphicsDropShadowEffect, QPainterPath, QPainterPathStroker,
[11255]7    QPolygonF, QColor, QPen
[11102]8)
9
10from PyQt4.QtCore import (
[11185]11    Qt, QPointF, QSizeF, QRectF, QLineF, QEvent, qVersion
[11102]12)
13
[11160]14from PyQt4.QtCore import pyqtSignal as Signal
[11273]15from PyQt4.QtCore import pyqtProperty as Property
[11102]16
17log = logging.getLogger(__name__)
18
[11160]19from .graphicspathobject import GraphicsPathObject
[11102]20
21
22class Annotation(QGraphicsWidget):
23    """Base class for annotations in the canvas scheme.
24    """
25    def __init__(self, parent=None, **kwargs):
26        QGraphicsWidget.__init__(self, parent, **kwargs)
27
[11172]28    if qVersion() < "4.7":
29        geometryChanged = Signal()
30
31        def setGeometry(self, rect):
32            QGraphicsWidget.setGeometry(self, rect)
33            self.geometryChanged.emit()
34
[11102]35
[11273]36class GraphicsTextEdit(QGraphicsTextItem):
37    """
38    QGraphicsTextItem subclass defining an additional placeholderText
39    property (text displayed when no text is set).
40
41    """
42    def __init__(self, *args, **kwargs):
43        QGraphicsTextItem.__init__(self, *args, **kwargs)
44
45        self.__placeholderText = ""
46
47    def setPlaceholderText(self, text):
48        """
49        Set the placeholder text. This is shown when the item has no text,
50        i.e when `toPlainText()` returns an empty string.
51
52        """
53        if self.__placeholderText != text:
54            self.__placeholderText = text
55            if not self.toPlainText():
56                self.update()
57
58    def placeholderText(self):
59        """
60        Return the placeholder text.
61        """
62        return unicode(self.__placeholderText)
63
64    placeholderText_ = Property(unicode, placeholderText, setPlaceholderText)
65
66    def paint(self, painter, option, widget=None):
67        QGraphicsTextItem.paint(self, painter, option, widget)
68
69        # Draw placeholder text if necessary
70        if not (self.toPlainText() and self.toHtml()) and \
71                self.__placeholderText and \
72                not (self.hasFocus() and \
73                     self.textInteractionFlags() & Qt.TextEditable):
74            brect = self.boundingRect()
75            metrics = painter.fontMetrics()
76            text = metrics.elidedText(self.__placeholderText, Qt.ElideRight,
77                                      brect.width())
78            color = self.defaultTextColor()
79            color.setAlpha(min(color.alpha(), 150))
80            painter.setPen(QPen(color))
81            painter.drawText(brect, Qt.AlignTop | Qt.AlignLeft, text)
82
83
[11102]84class TextAnnotation(Annotation):
[11161]85    """Text annotation item for the canvas scheme.
[11102]86
87    """
[11151]88    editingFinished = Signal()
[11161]89    """Emitted when the editing is finished (i.e. the item loses focus)."""
90
[11151]91    textEdited = Signal()
[11161]92    """Emitted when the edited text changes."""
[11102]93
94    def __init__(self, parent=None, **kwargs):
95        Annotation.__init__(self, parent, **kwargs)
96        self.setFlag(QGraphicsItem.ItemIsMovable)
[11185]97        self.setFlag(QGraphicsItem.ItemIsSelectable)
[11102]98
99        self.setFocusPolicy(Qt.ClickFocus)
100
101        self.__textMargins = (2, 2, 2, 2)
102
103        rect = self.geometry().translated(-self.pos())
104        self.__framePathItem = QGraphicsPathItem(self)
[11161]105        self.__framePathItem.setPen(QPen(Qt.NoPen))
[11102]106
[11273]107        self.__textItem = GraphicsTextEdit(self)
108        self.__textItem.setPlaceholderText(self.tr("Enter text here"))
[11102]109        self.__textItem.setPos(2, 2)
110        self.__textItem.setTextWidth(rect.width() - 4)
111        self.__textItem.setTabChangesFocus(True)
[11192]112        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
[11193]113        self.__textItem.setFont(self.font())
[11102]114        self.__textInteractionFlags = Qt.NoTextInteraction
115
116        layout = self.__textItem.document().documentLayout()
117        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
118
119        self.__updateFrame()
120
121    def adjustSize(self):
122        """Resize to a reasonable size.
123        """
124        self.__textItem.setTextWidth(-1)
125        self.__textItem.adjustSize()
126        size = self.__textItem.boundingRect().size()
127        left, top, right, bottom = self.textMargins()
128        geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom))
129        self.setGeometry(geom)
130
[11161]131    def setFramePen(self, pen):
132        """Set the frame pen. By default Qt.NoPen is used (i.e. the frame
133        is not shown).
134
135        """
136        self.__framePathItem.setPen(pen)
137
138    def framePen(self):
139        """Return the frame pen.
140        """
141        return self.__framePathItem.pen()
142
[11185]143    def setFrameBrush(self, brush):
144        """Set the frame brush.
145        """
146        self.__framePathItem.setBrush(brush)
147
148    def frameBrush(self):
149        """Return the frame brush.
150        """
151        return self.__framePathItem.brush()
152
[11102]153    def setPlainText(self, text):
154        """Set the annotation plain text.
155        """
156        self.__textItem.setPlainText(text)
157
158    def toPlainText(self):
159        return self.__textItem.toPlainText()
160
161    def setHtml(self, text):
162        """Set the annotation rich text.
163        """
164        self.__textItem.setHtml(text)
165
166    def toHtml(self):
167        return self.__textItem.toHtml()
168
169    def setDefaultTextColor(self, color):
170        """Set the default text color.
171        """
172        self.__textItem.setDefaultTextColor(color)
173
174    def defaultTextColor(self):
175        return self.__textItem.defaultTextColor()
176
177    def setTextMargins(self, left, top, right, bottom):
178        """Set the text margins.
179        """
180        margins = (left, top, right, bottom)
181        if self.__textMargins != margins:
182            self.__textMargins = margins
183            self.__textItem.setPos(left, top)
184            self.__textItem.setTextWidth(
185                max(self.geometry().width() - left - right, 0)
186            )
187
188    def textMargins(self):
189        """Return the text margins.
190        """
191        return self.__textMargins
192
193    def document(self):
194        """Return the QTextDocument instance used internally.
195        """
196        return self.__textItem.document()
197
198    def setTextCursor(self, cursor):
199        self.__textItem.setTextCursor(cursor)
200
201    def textCursor(self):
202        return self.__textItem.textCursor()
203
204    def setTextInteractionFlags(self, flags):
205        self.__textInteractionFlags = flags
206
207    def textInteractionFlags(self):
208        return self.__textInteractionFlags
209
210    def setDefaultStyleSheet(self, stylesheet):
211        self.document().setDefaultStyleSheet(stylesheet)
212
213    def mouseDoubleClickEvent(self, event):
214        Annotation.mouseDoubleClickEvent(self, event)
215
216        if event.buttons() == Qt.LeftButton and \
217                self.__textInteractionFlags & Qt.TextEditable:
218            self.startEdit()
219
220    def startEdit(self):
221        """Start the annotation text edit process.
222        """
[11192]223        self.__textItem.setTextInteractionFlags(self.__textInteractionFlags)
[11102]224        self.__textItem.setFocus(Qt.MouseFocusReason)
225
226        # Install event filter to find out when the text item loses focus.
227        self.__textItem.installSceneEventFilter(self)
[11151]228        self.__textItem.document().contentsChanged.connect(
229            self.textEdited
230        )
[11102]231
232    def endEdit(self):
233        """End the annotation edit.
234        """
235        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
236        self.__textItem.removeSceneEventFilter(self)
[11151]237        self.__textItem.document().contentsChanged.disconnect(
238            self.textEdited
239        )
240        self.editingFinished.emit()
[11102]241
242    def __onDocumentSizeChanged(self, size):
243        # The size of the text document has changed. Expand the text
244        # control rect's height if the text no longer fits inside.
245        try:
246            rect = self.geometry()
247            _, top, _, bottom = self.textMargins()
248            if rect.height() < (size.height() + bottom + top):
249                rect.setHeight(size.height() + bottom + top)
250                self.setGeometry(rect)
251        except Exception:
252            log.error("error in __onDocumentSizeChanged",
253                      exc_info=True)
254
255    def __updateFrame(self):
256        rect = self.geometry()
257        rect.moveTo(0, 0)
258        path = QPainterPath()
259        path.addRect(rect)
260        self.__framePathItem.setPath(path)
261
262    def resizeEvent(self, event):
263        width = event.newSize().width()
264        left, _, right, _ = self.textMargins()
265        self.__textItem.setTextWidth(max(width - left - right, 0))
266        self.__updateFrame()
267        QGraphicsWidget.resizeEvent(self, event)
268
269    def sceneEventFilter(self, obj, event):
270        if obj is self.__textItem and event.type() == QEvent.FocusOut:
271            self.__textItem.focusOutEvent(event)
272            self.endEdit()
273            return True
274
275        return Annotation.sceneEventFilter(self, obj, event)
276
[11185]277    def itemChange(self, change, value):
278        if change == QGraphicsItem.ItemSelectedHasChanged:
279            if self.isSelected():
280                self.setFramePen(QPen(Qt.DashDotLine))
281            else:
282                self.setFramePen(QPen(Qt.NoPen))
283
284        return Annotation.itemChange(self, change, value)
285
[11193]286    def changeEvent(self, event):
287        if event.type() == QEvent.FontChange:
288            self.__textItem.setFont(self.font())
289
290        Annotation.changeEvent(self, event)
291
[11102]292
293class ArrowItem(GraphicsPathObject):
[11230]294
295    #: Arrow Style
296    Plain, Concave = 1, 2
297
[11102]298    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
299        GraphicsPathObject.__init__(self, parent, **kwargs)
300
301        if line is None:
302            line = QLineF(0, 0, 10, 0)
303
304        self.__line = line
305
306        self.__lineWidth = lineWidth
307
[11230]308        self.__arrowStyle = ArrowItem.Plain
309
[11102]310        self.__updateArrowPath()
311
312    def setLine(self, line):
[11230]313        """Set the baseline of the arrow (:class:`QLineF`).
314        """
[11102]315        if self.__line != line:
[11161]316            self.__line = QLineF(line)
[11102]317            self.__updateArrowPath()
318
319    def line(self):
[11230]320        """Return the baseline of the arrow.
321        """
[11161]322        return QLineF(self.__line)
[11102]323
324    def setLineWidth(self, lineWidth):
[11230]325        """Set the width of the arrow.
326        """
[11102]327        if self.__lineWidth != lineWidth:
328            self.__lineWidth = lineWidth
329            self.__updateArrowPath()
330
331    def lineWidth(self):
[11230]332        """Return the width of the arrow.
333        """
[11102]334        return self.__lineWidth
335
[11230]336    def setArrowStyle(self, style):
337        """Set the arrow style (`ArrowItem.Plain` or `ArrowItem.Concave`)
338        """
339        if self.__arrowStyle != style:
340            self.__arrowStyle = style
341            self.__updateArrowPath()
342
343    def arrowStyle(self):
344        """Return the arrow style
345        """
346        return self.__arrowStyle
347
[11102]348    def __updateArrowPath(self):
[11230]349        if self.__arrowStyle == ArrowItem.Plain:
[11400]350            path = arrow_path_plain(self.__line, self.__lineWidth)
[11230]351        else:
[11400]352            path = arrow_path_concave(self.__line, self.__lineWidth)
[11230]353        self.setPath(path)
354
355
[11400]356def arrow_path_plain(line, width):
357    """
358    Return an :class:`QPainterPath` of a plain looking arrow.
359    """
360    path = QPainterPath()
361    p1, p2 = line.p1(), line.p2()
[11102]362
[11400]363    if p1 == p2:
[11230]364        return path
365
[11400]366    baseline = QLineF(line)
367    # Require some minimum length.
368    baseline.setLength(max(line.length() - width * 3, width * 3))
369    path.moveTo(baseline.p1())
370    path.lineTo(baseline.p2())
[11230]371
[11400]372    stroker = QPainterPathStroker()
373    stroker.setWidth(width)
374    path = stroker.createStroke(path)
[11230]375
[11400]376    arrow_head_len = width * 4
377    arrow_head_angle = 50
378    line_angle = line.angle() - 180
[11230]379
[11400]380    angle_1 = line_angle - arrow_head_angle / 2.0
381    angle_2 = line_angle + arrow_head_angle / 2.0
[11230]382
[11400]383    points = [p2,
384              p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
385              p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
386              p2]
[11230]387
[11400]388    poly = QPolygonF(points)
389    path_head = QPainterPath()
390    path_head.addPolygon(poly)
391    path = path.united(path_head)
392    return path
[11230]393
394
[11400]395def arrow_path_concave(line, width):
396    """
397    Return a :class:`QPainterPath` of a pretty looking arrow.
398    """
399    path = QPainterPath()
400    p1, p2 = line.p1(), line.p2()
[11230]401
[11400]402    if p1 == p2:
403        return path
[11230]404
[11400]405    baseline = QLineF(line)
406    # Require some minimum length.
407    baseline.setLength(max(line.length() - width * 3, width * 3))
[11230]408
[11400]409    start, end = baseline.p1(), baseline.p2()
410    mid = (start + end) / 2.0
411    normal = QLineF.fromPolar(1.0, baseline.angle() + 90).p2()
412
413    path.moveTo(start)
414    path.lineTo(start + (normal * width / 4.0))
415
416    path.quadTo(mid + (normal * width / 4.0),
417                end + (normal * width / 1.5))
418
419    path.lineTo(end - (normal * width / 1.5))
420    path.quadTo(mid - (normal * width / 4.0),
421                start - (normal * width / 4.0))
422    path.closeSubpath()
423
424    arrow_head_len = width * 4
425    arrow_head_angle = 50
426    line_angle = line.angle() - 180
427
428    angle_1 = line_angle - arrow_head_angle / 2.0
429    angle_2 = line_angle + arrow_head_angle / 2.0
430
431    points = [p2,
432              p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
433              baseline.p2(),
434              p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
435              p2]
436
437    poly = QPolygonF(points)
438    path_head = QPainterPath()
439    path_head.addPolygon(poly)
440    path = path.united(path_head)
441    return path
[11102]442
443
444class ArrowAnnotation(Annotation):
445    def __init__(self, parent=None, line=None, **kwargs):
446        Annotation.__init__(self, parent, **kwargs)
447        self.setFlag(QGraphicsItem.ItemIsMovable)
[11185]448        self.setFlag(QGraphicsItem.ItemIsSelectable)
449
[11102]450        self.setFocusPolicy(Qt.ClickFocus)
451
452        if line is None:
453            line = QLineF(0, 0, 20, 0)
454
455        self.__line = line
[11185]456        self.__color = QColor(Qt.red)
[11102]457        self.__arrowItem = ArrowItem(self)
458        self.__arrowItem.setLine(line)
[11185]459        self.__arrowItem.setBrush(self.__color)
[11161]460        self.__arrowItem.setPen(QPen(Qt.NoPen))
[11230]461        self.__arrowItem.setArrowStyle(ArrowItem.Concave)
462        self.__arrowItem.setLineWidth(5)
[11102]463
[11185]464        self.__shadow = QGraphicsDropShadowEffect(
[11230]465            blurRadius=5, offset=QPointF(1.0, 2.0),
[11185]466        )
467
468        self.__arrowItem.setGraphicsEffect(self.__shadow)
469        self.__shadow.setEnabled(True)
470
[11401]471        self.__autoAdjustGeometry = True
472
473    def setAutoAdjustGeometry(self, autoAdjust):
474        """
475        If set to `True` then the geometry will be adjusted whenever
476        the arrow is changed with `setLine`. Otherwise the geometry
477        of the item is only updated so the `line` lies within the
478        `geometry()` rect (i.e. it only grows). True by default
479
480        """
481        self.__autoAdjustGeometry = autoAdjust
482        if autoAdjust:
483            self.adjustGeometry()
484
485    def autoAdjustGeometry(self):
486        """
487        Should the geometry of the item be adjusted automatically when
488        `setLine` is called.
489
490        """
491        return self.__autoAdjustGeometry
492
[11102]493    def setLine(self, line):
[11401]494        """
495        Set the arrow base line (a `QLineF` in object coordinates).
[11102]496        """
497        if self.__line != line:
498            self.__line = line
499
[11255]500            # local item coordinate system
[11102]501            geom = self.geometry().translated(-self.pos())
502
503            if geom.isNull() and not line.isNull():
504                geom = QRectF(0, 0, 1, 1)
505
[11401]506            arrow_shape = arrow_path_concave(line, self.lineWidth())
507            arrow_rect = arrow_shape.boundingRect()
[11102]508
[11401]509            if not (geom.contains(arrow_rect)):
510                geom = geom.united(arrow_rect)
511
512            if self.__autoAdjustGeometry:
513                # Shrink the geometry if required.
514                geom = geom.intersected(arrow_rect)
515
516            # topLeft can move changing the local coordinates.
[11102]517            diff = geom.topLeft()
518            line = QLineF(line.p1() - diff, line.p2() - diff)
519            self.__arrowItem.setLine(line)
520            self.__line = line
521
[11255]522            # parent item coordinate system
[11102]523            geom.translate(self.pos())
524            self.setGeometry(geom)
525
[11255]526    def line(self):
[11401]527        """
528        Return the arrow base line (`QLineF` in object coordinates).
[11255]529        """
530        return QLineF(self.__line)
531
[11185]532    def setColor(self, color):
[11401]533        """
534        Set arrow brush color.
[11255]535        """
[11185]536        if self.__color != color:
537            self.__color = QColor(color)
538            self.__updateBrush()
539
540    def color(self):
[11401]541        """
542        Return the arrow brush color.
[11255]543        """
[11185]544        return QColor(self.__color)
545
[11102]546    def setLineWidth(self, lineWidth):
[11401]547        """
548        Set the arrow line width.
[11185]549        """
[11102]550        self.__arrowItem.setLineWidth(lineWidth)
551
552    def lineWidth(self):
[11401]553        """
554        Return the arrow line width.
[11185]555        """
[11102]556        return self.__arrowItem.lineWidth()
557
[11255]558    def adjustGeometry(self):
[11401]559        """
560        Adjust the widget geometry to exactly fit the arrow inside
[11255]561        while preserving the arrow path scene geometry.
562
563        """
564        # local system coordinate
565        geom = self.geometry().translated(-self.pos())
566        line = self.__line
567
568        arrow_rect = self.__arrowItem.shape().boundingRect()
569
570        if geom.isNull() and not line.isNull():
571            geom = QRectF(0, 0, 1, 1)
572
573        if not (geom.contains(arrow_rect)):
574            geom = geom.united(arrow_rect)
575
576        geom = geom.intersected(arrow_rect)
577        diff = geom.topLeft()
578        line = QLineF(line.p1() - diff, line.p2() - diff)
579        geom.translate(self.pos())
580        self.setGeometry(geom)
581        self.setLine(line)
582
[11102]583    def shape(self):
584        arrow_shape = self.__arrowItem.shape()
585        return self.mapFromItem(self.__arrowItem, arrow_shape)
586
[11185]587    def itemChange(self, change, value):
588        if change == QGraphicsItem.ItemSelectedHasChanged:
589            self.__updateBrush()
590
591        return Annotation.itemChange(self, change, value)
592
593    def __updateBrush(self):
[11401]594        """
595        Update the arrow brush.
[11185]596        """
597        if self.isSelected():
598            color = self.__color.darker(150)
599        else:
600            color = self.__color
601
602        self.__arrowItem.setBrush(color)
Note: See TracBrowser for help on using the repository browser.