source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11444:ec0f7723c7c2

Revision 11444:ec0f7723c7c2, 17.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Draw the Annotation placeholder text using the annotation's font.

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