source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11192:d51939aa6f45

Revision 11192:d51939aa6f45, 12.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Changed annotation item selection and (control point) geometry editing.

Control point editing is now fixed to the items focus 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.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
63        self.__textInteractionFlags = Qt.NoTextInteraction
64
65        layout = self.__textItem.document().documentLayout()
66        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
67
68        self.__updateFrame()
69
70    def adjustSize(self):
71        """Resize to a reasonable size.
72        """
73        self.__textItem.setTextWidth(-1)
74        self.__textItem.adjustSize()
75        size = self.__textItem.boundingRect().size()
76        left, top, right, bottom = self.textMargins()
77        geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom))
78        self.setGeometry(geom)
79
80    def setFramePen(self, pen):
81        """Set the frame pen. By default Qt.NoPen is used (i.e. the frame
82        is not shown).
83
84        """
85        self.__framePathItem.setPen(pen)
86
87    def framePen(self):
88        """Return the frame pen.
89        """
90        return self.__framePathItem.pen()
91
92    def setFrameBrush(self, brush):
93        """Set the frame brush.
94        """
95        self.__framePathItem.setBrush(brush)
96
97    def frameBrush(self):
98        """Return the frame brush.
99        """
100        return self.__framePathItem.brush()
101
102    def setPlainText(self, text):
103        """Set the annotation plain text.
104        """
105        self.__textItem.setPlainText(text)
106
107    def toPlainText(self):
108        return self.__textItem.toPlainText()
109
110    def setHtml(self, text):
111        """Set the annotation rich text.
112        """
113        self.__textItem.setHtml(text)
114
115    def toHtml(self):
116        return self.__textItem.toHtml()
117
118    def setDefaultTextColor(self, color):
119        """Set the default text color.
120        """
121        self.__textItem.setDefaultTextColor(color)
122
123    def defaultTextColor(self):
124        return self.__textItem.defaultTextColor()
125
126    def setTextMargins(self, left, top, right, bottom):
127        """Set the text margins.
128        """
129        margins = (left, top, right, bottom)
130        if self.__textMargins != margins:
131            self.__textMargins = margins
132            self.__textItem.setPos(left, top)
133            self.__textItem.setTextWidth(
134                max(self.geometry().width() - left - right, 0)
135            )
136
137    def textMargins(self):
138        """Return the text margins.
139        """
140        return self.__textMargins
141
142    def document(self):
143        """Return the QTextDocument instance used internally.
144        """
145        return self.__textItem.document()
146
147    def setTextCursor(self, cursor):
148        self.__textItem.setTextCursor(cursor)
149
150    def textCursor(self):
151        return self.__textItem.textCursor()
152
153    def setTextInteractionFlags(self, flags):
154        self.__textInteractionFlags = flags
155
156    def textInteractionFlags(self):
157        return self.__textInteractionFlags
158
159    def setDefaultStyleSheet(self, stylesheet):
160        self.document().setDefaultStyleSheet(stylesheet)
161
162    def mouseDoubleClickEvent(self, event):
163        Annotation.mouseDoubleClickEvent(self, event)
164
165        if event.buttons() == Qt.LeftButton and \
166                self.__textInteractionFlags & Qt.TextEditable:
167            self.startEdit()
168
169    def startEdit(self):
170        """Start the annotation text edit process.
171        """
172        self.__textItem.setTextInteractionFlags(self.__textInteractionFlags)
173        self.__textItem.setFocus(Qt.MouseFocusReason)
174
175        # Install event filter to find out when the text item loses focus.
176        self.__textItem.installSceneEventFilter(self)
177        self.__textItem.document().contentsChanged.connect(
178            self.textEdited
179        )
180
181    def endEdit(self):
182        """End the annotation edit.
183        """
184        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
185        self.__textItem.removeSceneEventFilter(self)
186        self.__textItem.document().contentsChanged.disconnect(
187            self.textEdited
188        )
189        self.editingFinished.emit()
190
191    def __onDocumentSizeChanged(self, size):
192        # The size of the text document has changed. Expand the text
193        # control rect's height if the text no longer fits inside.
194        try:
195            rect = self.geometry()
196            _, top, _, bottom = self.textMargins()
197            if rect.height() < (size.height() + bottom + top):
198                rect.setHeight(size.height() + bottom + top)
199                self.setGeometry(rect)
200        except Exception:
201            log.error("error in __onDocumentSizeChanged",
202                      exc_info=True)
203
204    def __updateFrame(self):
205        rect = self.geometry()
206        rect.moveTo(0, 0)
207        path = QPainterPath()
208        path.addRect(rect)
209        self.__framePathItem.setPath(path)
210
211    def resizeEvent(self, event):
212        width = event.newSize().width()
213        left, _, right, _ = self.textMargins()
214        self.__textItem.setTextWidth(max(width - left - right, 0))
215        self.__updateFrame()
216        QGraphicsWidget.resizeEvent(self, event)
217
218    def sceneEventFilter(self, obj, event):
219        if obj is self.__textItem and event.type() == QEvent.FocusOut:
220            self.__textItem.focusOutEvent(event)
221            self.endEdit()
222            return True
223
224        return Annotation.sceneEventFilter(self, obj, event)
225
226    def itemChange(self, change, value):
227        if change == QGraphicsItem.ItemSelectedHasChanged:
228            if self.isSelected():
229                self.setFramePen(QPen(Qt.DashDotLine))
230            else:
231                self.setFramePen(QPen(Qt.NoPen))
232
233        return Annotation.itemChange(self, change, value)
234
235
236class ArrowItem(GraphicsPathObject):
237    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
238        GraphicsPathObject.__init__(self, parent, **kwargs)
239
240        if line is None:
241            line = QLineF(0, 0, 10, 0)
242
243        self.__line = line
244
245        self.__lineWidth = lineWidth
246
247        self.__updateArrowPath()
248
249    def setLine(self, line):
250        if self.__line != line:
251            self.__line = QLineF(line)
252            self.__updateArrowPath()
253
254    def line(self):
255        return QLineF(self.__line)
256
257    def setLineWidth(self, lineWidth):
258        if self.__lineWidth != lineWidth:
259            self.__lineWidth = lineWidth
260            self.__updateArrowPath()
261
262    def lineWidth(self):
263        return self.__lineWidth
264
265    def __updateArrowPath(self):
266        line = self.__line
267        width = self.__lineWidth
268        path = QPainterPath()
269        p1, p2 = line.p1(), line.p2()
270        if p1 == p2:
271            self.setPath(path)
272            return
273
274        baseline = QLineF(line)
275        baseline.setLength(max(line.length() - width * 3, width * 3))
276        path.moveTo(baseline.p1())
277        path.lineTo(baseline.p2())
278
279        stroker = QPainterPathStroker()
280        stroker.setWidth(width)
281        path = stroker.createStroke(path)
282
283        arrow_head_len = width * 4
284        arrow_head_angle = 60
285        line_angle = line.angle() - 180
286
287        angle_1 = line_angle - arrow_head_angle / 2.0
288        angle_2 = line_angle + arrow_head_angle / 2.0
289
290        points = [p2,
291                  p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
292                  p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
293                  p2]
294        poly = QPolygonF(points)
295        path_head = QPainterPath()
296        path_head.addPolygon(poly)
297        path = path.united(path_head)
298        self.setPath(path)
299
300
301class ArrowAnnotation(Annotation):
302    def __init__(self, parent=None, line=None, **kwargs):
303        Annotation.__init__(self, parent, **kwargs)
304        self.setFlag(QGraphicsItem.ItemIsMovable)
305        self.setFlag(QGraphicsItem.ItemIsSelectable)
306
307        self.setFocusPolicy(Qt.ClickFocus)
308
309        if line is None:
310            line = QLineF(0, 0, 20, 0)
311
312        self.__line = line
313        self.__color = QColor(Qt.red)
314        self.__arrowItem = ArrowItem(self)
315        self.__arrowItem.setLine(line)
316        self.__arrowItem.setBrush(self.__color)
317        self.__arrowItem.setPen(QPen(Qt.NoPen))
318
319        self.__shadow = QGraphicsDropShadowEffect(
320            blurRadius=5, offset=QPointF(0.05, 0.05)
321        )
322
323        self.__arrowItem.setGraphicsEffect(self.__shadow)
324        self.__shadow.setEnabled(True)
325
326    def setLine(self, line):
327        """Set the arrow base line (a `QLineF` in object coordinates).
328        """
329        if self.__line != line:
330            self.__line = line
331
332            geom = self.geometry().translated(-self.pos())
333
334            if geom.isNull() and not line.isNull():
335                geom = QRectF(0, 0, 1, 1)
336            line_rect = QRectF(line.p1(), line.p2()).normalized()
337
338            if not (geom.contains(line_rect)):
339                geom = geom.united(line_rect)
340
341            diff = geom.topLeft()
342            line = QLineF(line.p1() - diff, line.p2() - diff)
343            self.__arrowItem.setLine(line)
344            self.__line = line
345
346            geom.translate(self.pos())
347            self.setGeometry(geom)
348
349    def setColor(self, color):
350        if self.__color != color:
351            self.__color = QColor(color)
352            self.__updateBrush()
353
354    def color(self):
355        return QColor(self.__color)
356
357    def adjustGeometry(self):
358        """Adjust the widget geometry to exactly fit the arrow inside
359        while preserving the arrow path scene geometry.
360
361        """
362        geom = self.geometry().translated(-self.pos())
363        line = self.__line
364        line_rect = QRectF(line.p1(), line.p2()).normalized()
365        if geom.isNull() and not line.isNull():
366            geom = QRectF(0, 0, 1, 1)
367        if not (geom.contains(line_rect)):
368            geom = geom.united(line_rect)
369        geom = geom.intersected(line_rect)
370        diff = geom.topLeft()
371        line = QLineF(line.p1() - diff, line.p2() - diff)
372        geom.translate(self.pos())
373        self.setGeometry(geom)
374        self.setLine(line)
375
376    def line(self):
377        return QLineF(self.__line)
378
379    def setLineWidth(self, lineWidth):
380        """Set the arrow line width.
381        """
382        self.__arrowItem.setLineWidth(lineWidth)
383
384    def lineWidth(self):
385        """Return the arrow line width.
386        """
387        return self.__arrowItem.lineWidth()
388
389    def shape(self):
390        arrow_shape = self.__arrowItem.shape()
391        return self.mapFromItem(self.__arrowItem, arrow_shape)
392
393    def itemChange(self, change, value):
394        if change == QGraphicsItem.ItemSelectedHasChanged:
395            self.__updateBrush()
396
397        return Annotation.itemChange(self, change, value)
398
399    def __updateBrush(self):
400        """Update the arrow brush.
401        """
402        if self.isSelected():
403            color = self.__color.darker(150)
404        else:
405            color = self.__color
406
407        self.__arrowItem.setBrush(color)
Note: See TracBrowser for help on using the repository browser.