source: orange/Orange/OrangeCanvas/canvas/items/annotationitem.py @ 11160:6bfea7812243

Revision 11160:6bfea7812243, 13.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Refactored GraphicsPathObject and ControlPoint/Rect/Line into two new modules.

Line 
1
2import logging
3
4from PyQt4.QtGui import (
5    QGraphicsItem, QGraphicsPathItem, QGraphicsWidget,
6    QGraphicsTextItem, QPainterPath, QPainterPathStroker,
7    QPolygonF
8)
9
10from PyQt4.QtCore import (
11    Qt, QSizeF, QRectF, QLineF, QEvent
12)
13
14from PyQt4.QtCore import pyqtSignal as Signal
15
16log = logging.getLogger(__name__)
17
18from .graphicspathobject import GraphicsPathObject
19from .controlpoints import ControlPointLine, ControlPointRect
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
28
29class TextAnnotation(Annotation):
30    """Text annotation for the canvas scheme.
31
32    """
33    editingFinished = Signal()
34    textEdited = Signal()
35
36    def __init__(self, parent=None, **kwargs):
37        Annotation.__init__(self, parent, **kwargs)
38        self.setFlag(QGraphicsItem.ItemIsMovable)
39
40        self.setFocusPolicy(Qt.ClickFocus)
41
42        self.__textMargins = (2, 2, 2, 2)
43
44        rect = self.geometry().translated(-self.pos())
45        self.__framePathItem = QGraphicsPathItem(self)
46        self.__controlPoints = ControlPointRect(self)
47        self.__controlPoints.setRect(rect)
48        self.__controlPoints.rectEdited.connect(self.__onControlRectEdited)
49        self.geometryChanged.connect(self.__updateControlPoints)
50
51        self.__textItem = QGraphicsTextItem(self)
52        self.__textItem.setPos(2, 2)
53        self.__textItem.setTextWidth(rect.width() - 4)
54        self.__textItem.setTabChangesFocus(True)
55        self.__textInteractionFlags = Qt.NoTextInteraction
56
57        layout = self.__textItem.document().documentLayout()
58        layout.documentSizeChanged.connect(self.__onDocumentSizeChanged)
59
60        self.__updateFrame()
61
62        self.__controlPoints.hide()
63
64    def adjustSize(self):
65        """Resize to a reasonable size.
66        """
67        self.__textItem.setTextWidth(-1)
68        self.__textItem.adjustSize()
69        size = self.__textItem.boundingRect().size()
70        left, top, right, bottom = self.textMargins()
71        geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom))
72        self.setGeometry(geom)
73
74    def setPlainText(self, text):
75        """Set the annotation plain text.
76        """
77        self.__textItem.setPlainText(text)
78
79    def toPlainText(self):
80        return self.__textItem.toPlainText()
81
82    def setHtml(self, text):
83        """Set the annotation rich text.
84        """
85        self.__textItem.setHtml(text)
86
87    def toHtml(self):
88        return self.__textItem.toHtml()
89
90    def setDefaultTextColor(self, color):
91        """Set the default text color.
92        """
93        self.__textItem.setDefaultTextColor(color)
94
95    def defaultTextColor(self):
96        return self.__textItem.defaultTextColor()
97
98    def setTextMargins(self, left, top, right, bottom):
99        """Set the text margins.
100        """
101        margins = (left, top, right, bottom)
102        if self.__textMargins != margins:
103            self.__textMargins = margins
104            self.__textItem.setPos(left, top)
105            self.__textItem.setTextWidth(
106                max(self.geometry().width() - left - right, 0)
107            )
108
109    def textMargins(self):
110        """Return the text margins.
111        """
112        return self.__textMargins
113
114    def document(self):
115        """Return the QTextDocument instance used internally.
116        """
117        return self.__textItem.document()
118
119    def setTextCursor(self, cursor):
120        self.__textItem.setTextCursor(cursor)
121
122    def textCursor(self):
123        return self.__textItem.textCursor()
124
125    def setTextInteractionFlags(self, flags):
126        self.__textInteractionFlags = flags
127        if self.__textItem.hasFocus():
128            self.__textItem.setTextInteractionFlags(flags)
129
130    def textInteractionFlags(self):
131        return self.__textInteractionFlags
132
133    def setDefaultStyleSheet(self, stylesheet):
134        self.document().setDefaultStyleSheet(stylesheet)
135
136    def mouseDoubleClickEvent(self, event):
137        Annotation.mouseDoubleClickEvent(self, event)
138
139        if event.buttons() == Qt.LeftButton and \
140                self.__textInteractionFlags & Qt.TextEditable:
141            self.startEdit()
142
143    def focusInEvent(self, event):
144        # Reparent the control points item to the scene
145        self.__controlPoints.setParentItem(None)
146        self.__controlPoints.show()
147        self.__controlPoints.setZValue(self.zValue() + 3)
148        self.__updateControlPoints()
149        Annotation.focusInEvent(self, event)
150
151    def focusOutEvent(self, event):
152        self.__controlPoints.hide()
153        # Reparent back to self
154        self.__controlPoints.setParentItem(self)
155        Annotation.focusOutEvent(self, event)
156
157    def startEdit(self):
158        """Start the annotation text edit process.
159        """
160        self.__textItem.setTextInteractionFlags(
161                            self.__textInteractionFlags)
162        self.__textItem.setFocus(Qt.MouseFocusReason)
163
164        # Install event filter to find out when the text item loses focus.
165        self.__textItem.installSceneEventFilter(self)
166        self.__textItem.document().contentsChanged.connect(
167            self.textEdited
168        )
169
170    def endEdit(self):
171        """End the annotation edit.
172        """
173        if self.__textItem.hasFocus():
174            self.__textItem.clearFocus()
175
176        self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction)
177        self.__textItem.removeSceneEventFilter(self)
178        self.__textItem.document().contentsChanged.disconnect(
179            self.textEdited
180        )
181        self.editingFinished.emit()
182
183    def __onDocumentSizeChanged(self, size):
184        # The size of the text document has changed. Expand the text
185        # control rect's height if the text no longer fits inside.
186        try:
187            rect = self.geometry()
188            _, top, _, bottom = self.textMargins()
189            if rect.height() < (size.height() + bottom + top):
190                rect.setHeight(size.height() + bottom + top)
191                self.setGeometry(rect)
192        except Exception:
193            log.error("error in __onDocumentSizeChanged",
194                      exc_info=True)
195
196    def __onControlRectEdited(self, newrect):
197        # The control rect has been edited by the user
198        # new rect is ins scene coordinates
199        try:
200            newpos = newrect.topLeft()
201            parent = self.parentItem()
202            if parent:
203                newpos = parent.mapFromScene(newpos)
204
205            geom = QRectF(newpos, newrect.size())
206            self.setGeometry(geom)
207        except Exception:
208            log.error("An error occurred in '__onControlRectEdited'",
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 __updateControlPoints(self, *args):
219        """Update the control points geometry.
220        """
221        if not self.__controlPoints.isVisible():
222            return
223
224        try:
225            geom = self.geometry()
226            parent = self.parentItem()
227            # The control rect is in scene coordinates
228            if parent is not None:
229                geom = QRectF(parent.mapToScene(geom.topLeft()),
230                              geom.size())
231            self.__controlPoints.setRect(geom)
232        except Exception:
233            log.error("An error occurred in '__updateControlPoints'",
234                      exc_info=True)
235
236    def resizeEvent(self, event):
237        width = event.newSize().width()
238        left, _, right, _ = self.textMargins()
239        self.__textItem.setTextWidth(max(width - left - right, 0))
240        self.__updateFrame()
241        self.__updateControlPoints()
242        QGraphicsWidget.resizeEvent(self, event)
243
244    def sceneEventFilter(self, obj, event):
245        if obj is self.__textItem and event.type() == QEvent.FocusOut:
246            self.__textItem.focusOutEvent(event)
247            self.endEdit()
248            return True
249
250        return Annotation.sceneEventFilter(self, obj, event)
251
252
253class ArrowItem(GraphicsPathObject):
254    def __init__(self, parent=None, line=None, lineWidth=4, **kwargs):
255        GraphicsPathObject.__init__(self, parent, **kwargs)
256
257        if line is None:
258            line = QLineF(0, 0, 10, 0)
259
260        self.__line = line
261
262        self.__lineWidth = lineWidth
263
264        self.__updateArrowPath()
265
266    def setLine(self, line):
267        if self.__line != line:
268            self.__line = line
269            self.__updateArrowPath()
270
271    def line(self):
272        return self.__line
273
274    def setLineWidth(self, lineWidth):
275        if self.__lineWidth != lineWidth:
276            self.__lineWidth = lineWidth
277            self.__updateArrowPath()
278
279    def lineWidth(self):
280        return self.__lineWidth
281
282    def __updateArrowPath(self):
283        line = self.__line
284        width = self.__lineWidth
285        path = QPainterPath()
286        p1, p2 = line.p1(), line.p2()
287        if p1 == p2:
288            self.setPath(path)
289            return
290
291        baseline = QLineF(line)
292        baseline.setLength(max(line.length() - width * 3, width * 3))
293        path.moveTo(baseline.p1())
294        path.lineTo(baseline.p2())
295
296        stroker = QPainterPathStroker()
297        stroker.setWidth(width)
298        path = stroker.createStroke(path)
299
300        arrow_head_len = width * 4
301        arrow_head_angle = 60
302        line_angle = line.angle() - 180
303
304        angle_1 = line_angle - arrow_head_angle / 2.0
305        angle_2 = line_angle + arrow_head_angle / 2.0
306
307        points = [p2,
308                  p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
309                  p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
310                  p2]
311        poly = QPolygonF(points)
312        path_head = QPainterPath()
313        path_head.addPolygon(poly)
314        path = path.united(path_head)
315        self.setPath(path)
316
317
318class ArrowAnnotation(Annotation):
319    def __init__(self, parent=None, line=None, **kwargs):
320        Annotation.__init__(self, parent, **kwargs)
321        self.setFlag(QGraphicsItem.ItemIsMovable)
322        self.setFocusPolicy(Qt.ClickFocus)
323
324        if line is None:
325            line = QLineF(0, 0, 20, 0)
326
327        self.__line = line
328        self.__arrowItem = ArrowItem(self)
329        self.__arrowItem.setLine(line)
330        self.__arrowItem.setBrush(Qt.red)
331        self.__arrowItem.setPen(Qt.NoPen)
332        self.__controlPointLine = ControlPointLine(self)
333        self.__controlPointLine.setLine(line)
334        self.__controlPointLine.hide()
335        self.__controlPointLine.lineEdited.connect(self.__onLineEdited)
336
337    def setLine(self, line):
338        """Set the arrow base line (a QLineF in object coordinates).
339        """
340        if self.__line != line:
341            self.__line = line
342#            self.__arrowItem.setLine(line)
343            # Check if the line does not fit inside the geometry.
344
345            geom = self.geometry().translated(-self.pos())
346
347            if geom.isNull() and not line.isNull():
348                geom = QRectF(0, 0, 1, 1)
349            line_rect = QRectF(line.p1(), line.p2())
350
351            if not (geom.contains(line_rect)):
352                geom = geom.united(line_rect)
353
354            diff = geom.topLeft()
355            line = QLineF(line.p1() - diff, line.p2() - diff)
356            self.__arrowItem.setLine(line)
357            self.__line = line
358
359            geom.translate(self.pos())
360            self.setGeometry(geom)
361
362    def adjustGeometry(self):
363        """Adjust the widget geometry to exactly fit the arrow inside
364        preserving the arrow path scene geometry.
365
366        """
367        geom = self.geometry().translated(-self.pos())
368        line = self.__line
369        line_rect = QRectF(line.p1(), line.p2()).normalized()
370        if geom.isNull() and not line.isNull():
371            geom = QRectF(0, 0, 1, 1)
372        if not (geom.contains(line_rect)):
373            geom = geom.united(line_rect)
374        geom = geom.intersected(line_rect)
375        diff = geom.topLeft()
376        line = QLineF(line.p1() - diff, line.p2() - diff)
377        geom.translate(self.pos())
378        self.setGeometry(geom)
379        self.setLine(line)
380
381    def line(self):
382        return self.__line
383
384    def setLineWidth(self, lineWidth):
385        self.__arrowItem.setLineWidth(lineWidth)
386
387    def lineWidth(self):
388        return self.__arrowItem.lineWidth()
389
390    def focusInEvent(self, event):
391        self.__controlPointLine.setParentItem(None)
392        self.__controlPointLine.show()
393        self.__controlPointLine.setZValue(self.zValue() + 3)
394        self.__updateControlLine()
395        self.geometryChanged.connect(self.__onGeometryChange)
396        return Annotation.focusInEvent(self, event)
397
398    def focusOutEvent(self, event):
399        self.__controlPointLine.hide()
400        self.__controlPointLine.setParentItem(self)
401        self.geometryChanged.disconnect(self.__onGeometryChange)
402        return Annotation.focusOutEvent(self, event)
403
404    def __updateControlLine(self):
405        if not self.__controlPointLine.isVisible():
406            return
407
408        line = self.__line
409        line = QLineF(self.mapToScene(line.p1()),
410                      self.mapToScene(line.p2()))
411        self.__controlPointLine.setLine(line)
412
413    def __onLineEdited(self, line):
414        line = QLineF(self.mapFromScene(line.p1()),
415                      self.mapFromScene(line.p2()))
416        self.setLine(line)
417
418    def __onGeometryChange(self):
419        if self.__controlPointLine.isVisible():
420            self.__updateControlLine()
421
422    def shape(self):
423        arrow_shape = self.__arrowItem.shape()
424        return self.mapFromItem(self.__arrowItem, arrow_shape)
425
426#    def paint(self, painter, option, widget=None):
427#        painter.drawRect(self.geometry().translated(-self.pos()))
428#        return Annotation.paint(self, painter, option, widget)
Note: See TracBrowser for help on using the repository browser.