source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11230:6b81f21ec0e9

Revision 11230:6b81f21ec0e9, 14.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Use prettier arrows for ArrowAnnotation.

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