source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11185:528087d3f1bb

Revision 11185:528087d3f1bb, 12.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Indicate annotation items's selection state.

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.__textInteractionFlags = Qt.NoTextInteraction
63
64        layout = self.__textItem.document().documentLayout()
65        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
66
67        self.setFocusProxy(self.__textItem)
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        if self.__textItem.hasFocus():
157            self.__textItem.setTextInteractionFlags(flags)
158
159    def textInteractionFlags(self):
160        return self.__textInteractionFlags
161
162    def setDefaultStyleSheet(self, stylesheet):
163        self.document().setDefaultStyleSheet(stylesheet)
164
165    def mouseDoubleClickEvent(self, event):
166        Annotation.mouseDoubleClickEvent(self, event)
167
168        if event.buttons() == Qt.LeftButton and \
169                self.__textInteractionFlags & Qt.TextEditable:
170            self.startEdit()
171
172    def startEdit(self):
173        """Start the annotation text edit process.
174        """
175        self.__textItem.setTextInteractionFlags(
176                            self.__textInteractionFlags)
177        self.__textItem.setFocus(Qt.MouseFocusReason)
178
179        # Install event filter to find out when the text item loses focus.
180        self.__textItem.installSceneEventFilter(self)
181        self.__textItem.document().contentsChanged.connect(
182            self.textEdited
183        )
184
185    def endEdit(self):
186        """End the annotation edit.
187        """
188        if self.__textItem.hasFocus():
189            self.__textItem.clearFocus()
190
191        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
192        self.__textItem.removeSceneEventFilter(self)
193        self.__textItem.document().contentsChanged.disconnect(
194            self.textEdited
195        )
196        self.editingFinished.emit()
197
198    def __onDocumentSizeChanged(self, size):
199        # The size of the text document has changed. Expand the text
200        # control rect's height if the text no longer fits inside.
201        try:
202            rect = self.geometry()
203            _, top, _, bottom = self.textMargins()
204            if rect.height() < (size.height() + bottom + top):
205                rect.setHeight(size.height() + bottom + top)
206                self.setGeometry(rect)
207        except Exception:
208            log.error("error in __onDocumentSizeChanged",
209                      exc_info=True)
210
211    def __updateFrame(self):
212        rect = self.geometry()
213        rect.moveTo(0, 0)
214        path = QPainterPath()
215        path.addRect(rect)
216        self.__framePathItem.setPath(path)
217
218    def resizeEvent(self, event):
219        width = event.newSize().width()
220        left, _, right, _ = self.textMargins()
221        self.__textItem.setTextWidth(max(width - left - right, 0))
222        self.__updateFrame()
223        QGraphicsWidget.resizeEvent(self, event)
224
225    def sceneEventFilter(self, obj, event):
226        if obj is self.__textItem and event.type() == QEvent.FocusOut:
227            self.__textItem.focusOutEvent(event)
228            self.endEdit()
229            return True
230
231        return Annotation.sceneEventFilter(self, obj, event)
232
233    def itemChange(self, change, value):
234        if change == QGraphicsItem.ItemSelectedHasChanged:
235            if self.isSelected():
236                self.setFramePen(QPen(Qt.DashDotLine))
237            else:
238                self.setFramePen(QPen(Qt.NoPen))
239
240        return Annotation.itemChange(self, change, value)
241
242
243class ArrowItem(GraphicsPathObject):
244    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
245        GraphicsPathObject.__init__(self, parent, **kwargs)
246
247        if line is None:
248            line = QLineF(0, 0, 10, 0)
249
250        self.__line = line
251
252        self.__lineWidth = lineWidth
253
254        self.__updateArrowPath()
255
256    def setLine(self, line):
257        if self.__line != line:
258            self.__line = QLineF(line)
259            self.__updateArrowPath()
260
261    def line(self):
262        return QLineF(self.__line)
263
264    def setLineWidth(self, lineWidth):
265        if self.__lineWidth != lineWidth:
266            self.__lineWidth = lineWidth
267            self.__updateArrowPath()
268
269    def lineWidth(self):
270        return self.__lineWidth
271
272    def __updateArrowPath(self):
273        line = self.__line
274        width = self.__lineWidth
275        path = QPainterPath()
276        p1, p2 = line.p1(), line.p2()
277        if p1 == p2:
278            self.setPath(path)
279            return
280
281        baseline = QLineF(line)
282        baseline.setLength(max(line.length() - width * 3, width * 3))
283        path.moveTo(baseline.p1())
284        path.lineTo(baseline.p2())
285
286        stroker = QPainterPathStroker()
287        stroker.setWidth(width)
288        path = stroker.createStroke(path)
289
290        arrow_head_len = width * 4
291        arrow_head_angle = 60
292        line_angle = line.angle() - 180
293
294        angle_1 = line_angle - arrow_head_angle / 2.0
295        angle_2 = line_angle + arrow_head_angle / 2.0
296
297        points = [p2,
298                  p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
299                  p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
300                  p2]
301        poly = QPolygonF(points)
302        path_head = QPainterPath()
303        path_head.addPolygon(poly)
304        path = path.united(path_head)
305        self.setPath(path)
306
307
308class ArrowAnnotation(Annotation):
309    def __init__(self, parent=None, line=None, **kwargs):
310        Annotation.__init__(self, parent, **kwargs)
311        self.setFlag(QGraphicsItem.ItemIsMovable)
312        self.setFlag(QGraphicsItem.ItemIsSelectable)
313
314        self.setFocusPolicy(Qt.ClickFocus)
315
316        if line is None:
317            line = QLineF(0, 0, 20, 0)
318
319        self.__line = line
320        self.__color = QColor(Qt.red)
321        self.__arrowItem = ArrowItem(self)
322        self.__arrowItem.setLine(line)
323        self.__arrowItem.setBrush(self.__color)
324        self.__arrowItem.setPen(QPen(Qt.NoPen))
325
326        self.__shadow = QGraphicsDropShadowEffect(
327            blurRadius=5, offset=QPointF(0.05, 0.05)
328        )
329
330        self.__arrowItem.setGraphicsEffect(self.__shadow)
331        self.__shadow.setEnabled(True)
332
333    def setLine(self, line):
334        """Set the arrow base line (a `QLineF` in object coordinates).
335        """
336        if self.__line != line:
337            self.__line = line
338
339            geom = self.geometry().translated(-self.pos())
340
341            if geom.isNull() and not line.isNull():
342                geom = QRectF(0, 0, 1, 1)
343            line_rect = QRectF(line.p1(), line.p2()).normalized()
344
345            if not (geom.contains(line_rect)):
346                geom = geom.united(line_rect)
347
348            diff = geom.topLeft()
349            line = QLineF(line.p1() - diff, line.p2() - diff)
350            self.__arrowItem.setLine(line)
351            self.__line = line
352
353            geom.translate(self.pos())
354            self.setGeometry(geom)
355
356    def setColor(self, color):
357        if self.__color != color:
358            self.__color = QColor(color)
359            self.__updateBrush()
360
361    def color(self):
362        return QColor(self.__color)
363
364    def adjustGeometry(self):
365        """Adjust the widget geometry to exactly fit the arrow inside
366        while preserving the arrow path scene geometry.
367
368        """
369        geom = self.geometry().translated(-self.pos())
370        line = self.__line
371        line_rect = QRectF(line.p1(), line.p2()).normalized()
372        if geom.isNull() and not line.isNull():
373            geom = QRectF(0, 0, 1, 1)
374        if not (geom.contains(line_rect)):
375            geom = geom.united(line_rect)
376        geom = geom.intersected(line_rect)
377        diff = geom.topLeft()
378        line = QLineF(line.p1() - diff, line.p2() - diff)
379        geom.translate(self.pos())
380        self.setGeometry(geom)
381        self.setLine(line)
382
383    def line(self):
384        return QLineF(self.__line)
385
386    def setLineWidth(self, lineWidth):
387        """Set the arrow line width.
388        """
389        self.__arrowItem.setLineWidth(lineWidth)
390
391    def lineWidth(self):
392        """Return the arrow line width.
393        """
394        return self.__arrowItem.lineWidth()
395
396    def shape(self):
397        arrow_shape = self.__arrowItem.shape()
398        return self.mapFromItem(self.__arrowItem, arrow_shape)
399
400    def itemChange(self, change, value):
401        if change == QGraphicsItem.ItemSelectedHasChanged:
402            self.__updateBrush()
403
404        return Annotation.itemChange(self, change, value)
405
406    def __updateBrush(self):
407        """Update the arrow brush.
408        """
409        if self.isSelected():
410            color = self.__color.darker(150)
411        else:
412            color = self.__color
413
414        self.__arrowItem.setBrush(color)
Note: See TracBrowser for help on using the repository browser.