source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11172:b9add0151621

Revision 11172:b9add0151621, 10.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Compatibility fixes for PyQt4 < 4.9

Line 
1
2import logging
3
4from PyQt4.QtGui import (
5    QGraphicsItem, QGraphicsPathItem, QGraphicsWidget,
6    QGraphicsTextItem, QPainterPath, QPainterPathStroker,
7    QPen, QPolygonF
8)
9
10from PyQt4.QtCore import (
11    Qt, 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
49        self.setFocusPolicy(Qt.ClickFocus)
50
51        self.__textMargins = (2, 2, 2, 2)
52
53        rect = self.geometry().translated(-self.pos())
54        self.__framePathItem = QGraphicsPathItem(self)
55        self.__framePathItem.setPen(QPen(Qt.NoPen))
56
57        self.__textItem = QGraphicsTextItem(self)
58        self.__textItem.setPos(2, 2)
59        self.__textItem.setTextWidth(rect.width() - 4)
60        self.__textItem.setTabChangesFocus(True)
61        self.__textInteractionFlags = Qt.NoTextInteraction
62
63        layout = self.__textItem.document().documentLayout()
64        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
65
66        self.setFocusProxy(self.__textItem)
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 setPlainText(self, text):
93        """Set the annotation plain text.
94        """
95        self.__textItem.setPlainText(text)
96
97    def toPlainText(self):
98        return self.__textItem.toPlainText()
99
100    def setHtml(self, text):
101        """Set the annotation rich text.
102        """
103        self.__textItem.setHtml(text)
104
105    def toHtml(self):
106        return self.__textItem.toHtml()
107
108    def setDefaultTextColor(self, color):
109        """Set the default text color.
110        """
111        self.__textItem.setDefaultTextColor(color)
112
113    def defaultTextColor(self):
114        return self.__textItem.defaultTextColor()
115
116    def setTextMargins(self, left, top, right, bottom):
117        """Set the text margins.
118        """
119        margins = (left, top, right, bottom)
120        if self.__textMargins != margins:
121            self.__textMargins = margins
122            self.__textItem.setPos(left, top)
123            self.__textItem.setTextWidth(
124                max(self.geometry().width() - left - right, 0)
125            )
126
127    def textMargins(self):
128        """Return the text margins.
129        """
130        return self.__textMargins
131
132    def document(self):
133        """Return the QTextDocument instance used internally.
134        """
135        return self.__textItem.document()
136
137    def setTextCursor(self, cursor):
138        self.__textItem.setTextCursor(cursor)
139
140    def textCursor(self):
141        return self.__textItem.textCursor()
142
143    def setTextInteractionFlags(self, flags):
144        self.__textInteractionFlags = flags
145        if self.__textItem.hasFocus():
146            self.__textItem.setTextInteractionFlags(flags)
147
148    def textInteractionFlags(self):
149        return self.__textInteractionFlags
150
151    def setDefaultStyleSheet(self, stylesheet):
152        self.document().setDefaultStyleSheet(stylesheet)
153
154    def mouseDoubleClickEvent(self, event):
155        Annotation.mouseDoubleClickEvent(self, event)
156
157        if event.buttons() == Qt.LeftButton and \
158                self.__textInteractionFlags & Qt.TextEditable:
159            self.startEdit()
160
161    def startEdit(self):
162        """Start the annotation text edit process.
163        """
164        self.__textItem.setTextInteractionFlags(
165                            self.__textInteractionFlags)
166        self.__textItem.setFocus(Qt.MouseFocusReason)
167
168        # Install event filter to find out when the text item loses focus.
169        self.__textItem.installSceneEventFilter(self)
170        self.__textItem.document().contentsChanged.connect(
171            self.textEdited
172        )
173
174    def endEdit(self):
175        """End the annotation edit.
176        """
177        if self.__textItem.hasFocus():
178            self.__textItem.clearFocus()
179
180        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
181        self.__textItem.removeSceneEventFilter(self)
182        self.__textItem.document().contentsChanged.disconnect(
183            self.textEdited
184        )
185        self.editingFinished.emit()
186
187    def __onDocumentSizeChanged(self, size):
188        # The size of the text document has changed. Expand the text
189        # control rect's height if the text no longer fits inside.
190        try:
191            rect = self.geometry()
192            _, top, _, bottom = self.textMargins()
193            if rect.height() < (size.height() + bottom + top):
194                rect.setHeight(size.height() + bottom + top)
195                self.setGeometry(rect)
196        except Exception:
197            log.error("error in __onDocumentSizeChanged",
198                      exc_info=True)
199
200    def __updateFrame(self):
201        rect = self.geometry()
202        rect.moveTo(0, 0)
203        path = QPainterPath()
204        path.addRect(rect)
205        self.__framePathItem.setPath(path)
206
207    def resizeEvent(self, event):
208        width = event.newSize().width()
209        left, _, right, _ = self.textMargins()
210        self.__textItem.setTextWidth(max(width - left - right, 0))
211        self.__updateFrame()
212        QGraphicsWidget.resizeEvent(self, event)
213
214    def sceneEventFilter(self, obj, event):
215        if obj is self.__textItem and event.type() == QEvent.FocusOut:
216            self.__textItem.focusOutEvent(event)
217            self.endEdit()
218            return True
219
220        return Annotation.sceneEventFilter(self, obj, event)
221
222
223class ArrowItem(GraphicsPathObject):
224    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
225        GraphicsPathObject.__init__(self, parent, **kwargs)
226
227        if line is None:
228            line = QLineF(0, 0, 10, 0)
229
230        self.__line = line
231
232        self.__lineWidth = lineWidth
233
234        self.__updateArrowPath()
235
236    def setLine(self, line):
237        if self.__line != line:
238            self.__line = QLineF(line)
239            self.__updateArrowPath()
240
241    def line(self):
242        return QLineF(self.__line)
243
244    def setLineWidth(self, lineWidth):
245        if self.__lineWidth != lineWidth:
246            self.__lineWidth = lineWidth
247            self.__updateArrowPath()
248
249    def lineWidth(self):
250        return self.__lineWidth
251
252    def __updateArrowPath(self):
253        line = self.__line
254        width = self.__lineWidth
255        path = QPainterPath()
256        p1, p2 = line.p1(), line.p2()
257        if p1 == p2:
258            self.setPath(path)
259            return
260
261        baseline = QLineF(line)
262        baseline.setLength(max(line.length() - width * 3, width * 3))
263        path.moveTo(baseline.p1())
264        path.lineTo(baseline.p2())
265
266        stroker = QPainterPathStroker()
267        stroker.setWidth(width)
268        path = stroker.createStroke(path)
269
270        arrow_head_len = width * 4
271        arrow_head_angle = 60
272        line_angle = line.angle() - 180
273
274        angle_1 = line_angle - arrow_head_angle / 2.0
275        angle_2 = line_angle + arrow_head_angle / 2.0
276
277        points = [p2,
278                  p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
279                  p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
280                  p2]
281        poly = QPolygonF(points)
282        path_head = QPainterPath()
283        path_head.addPolygon(poly)
284        path = path.united(path_head)
285        self.setPath(path)
286
287
288class ArrowAnnotation(Annotation):
289    def __init__(self, parent=None, line=None, **kwargs):
290        Annotation.__init__(self, parent, **kwargs)
291        self.setFlag(QGraphicsItem.ItemIsMovable)
292        self.setFocusPolicy(Qt.ClickFocus)
293
294        if line is None:
295            line = QLineF(0, 0, 20, 0)
296
297        self.__line = line
298        self.__arrowItem = ArrowItem(self)
299        self.__arrowItem.setLine(line)
300        self.__arrowItem.setBrush(Qt.red)
301        self.__arrowItem.setPen(QPen(Qt.NoPen))
302
303    def setLine(self, line):
304        """Set the arrow base line (a `QLineF` in object coordinates).
305        """
306        if self.__line != line:
307            self.__line = line
308
309            geom = self.geometry().translated(-self.pos())
310
311            if geom.isNull() and not line.isNull():
312                geom = QRectF(0, 0, 1, 1)
313            line_rect = QRectF(line.p1(), line.p2()).normalized()
314
315            if not (geom.contains(line_rect)):
316                geom = geom.united(line_rect)
317
318            diff = geom.topLeft()
319            line = QLineF(line.p1() - diff, line.p2() - diff)
320            self.__arrowItem.setLine(line)
321            self.__line = line
322
323            geom.translate(self.pos())
324            self.setGeometry(geom)
325
326    def adjustGeometry(self):
327        """Adjust the widget geometry to exactly fit the arrow inside
328        while preserving the arrow path scene geometry.
329
330        """
331        geom = self.geometry().translated(-self.pos())
332        line = self.__line
333        line_rect = QRectF(line.p1(), line.p2()).normalized()
334        if geom.isNull() and not line.isNull():
335            geom = QRectF(0, 0, 1, 1)
336        if not (geom.contains(line_rect)):
337            geom = geom.united(line_rect)
338        geom = geom.intersected(line_rect)
339        diff = geom.topLeft()
340        line = QLineF(line.p1() - diff, line.p2() - diff)
341        geom.translate(self.pos())
342        self.setGeometry(geom)
343        self.setLine(line)
344
345    def line(self):
346        return QLineF(self.__line)
347
348    def setLineWidth(self, lineWidth):
349        self.__arrowItem.setLineWidth(lineWidth)
350
351    def lineWidth(self):
352        return self.__arrowItem.lineWidth()
353
354    def shape(self):
355        arrow_shape = self.__arrowItem.shape()
356        return self.mapFromItem(self.__arrowItem, arrow_shape)
357
358#    def paint(self, painter, option, widget=None):
359#        painter.drawRect(self.geometry().translated(-self.pos()))
360#        return Annotation.paint(self, painter, option, widget)
Note: See TracBrowser for help on using the repository browser.