class TextAnnotation(Annotation): """ Text annotation item for the canvas scheme. Text interaction (if enabled) is started by double clicking the item. """ #: Emitted when the editing is finished (i.e. the item loses edit focus). editingFinished = Signal() #: Emitted when the text content changes on user interaction. textEdited = Signal() #: Emitted when the text annotation's contents change #: (`content` or `contentType` changed) contentChanged = Signal() def __init__(self, parent=None, **kwargs): # type: (Optional[QGraphicsItem], Any) -> None super().__init__(None, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) self.__contentType = "text/plain" self.__content = "" self.__textMargins = (2, 2, 2, 2) self.__textInteractionFlags = Qt.NoTextInteraction self.__defaultInteractionFlags = Qt.TextInteractionFlags( Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) rect = self.geometry().translated(-self.pos()) self.__framePen = QPen(Qt.NoPen) self.__framePathItem = QGraphicsPathItem(self) self.__framePathItem.setPen(self.__framePen) self.__textItem = GraphicsTextEdit(self) self.__textItem.setOpenExternalLinks(True) self.__textItem.setPlaceholderText(self.tr("Enter text here")) self.__textItem.setPos(2, 2) self.__textItem.setTextWidth(rect.width() - 4) self.__textItem.setTabChangesFocus(True) self.__textItem.setTextInteractionFlags(self.__defaultInteractionFlags) self.__textItem.setFont(self.font()) self.__textItem.editingFinished.connect(self.__textEditingFinished) self.__textItem.setDefaultTextColor(self.palette().color( QPalette.Text)) if self.__textItem.scene() is not None: self.__textItem.installSceneEventFilter(self) layout = self.__textItem.document().documentLayout() layout.documentSizeChanged.connect(self.__onDocumentSizeChanged) self.__updateFrame() # set parent item at the end in order to ensure # QGraphicsItem.ItemSceneHasChanged is delivered after initialization if parent is not None: self.setParentItem(parent) def itemChange(self, change, value): # type: (QGraphicsItem.GraphicsItemChange, Any) -> Any if change == QGraphicsItem.ItemSceneHasChanged: if self.__textItem.scene() is not None: self.__textItem.installSceneEventFilter(self) if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateFrameStyle() return super().itemChange(change, value) def adjustSize(self): # type: () -> None """Resize to a reasonable size. """ self.__textItem.setTextWidth(-1) self.__textItem.adjustSize() size = self.__textItem.boundingRect().size() left, top, right, bottom = self.textMargins() geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom)) self.setGeometry(geom) def setFramePen(self, pen): # type: (QPen) -> None """Set the frame pen. By default Qt.NoPen is used (i.e. the frame is not shown). """ if pen != self.__framePen: self.__framePen = QPen(pen) self.__updateFrameStyle() def framePen(self): # type: () -> QPen """Return the frame pen. """ return QPen(self.__framePen) def setFrameBrush(self, brush): # type: (QBrush) -> None """Set the frame brush. """ self.__framePathItem.setBrush(brush) def frameBrush(self): # type: () -> QBrush """Return the frame brush. """ return self.__framePathItem.brush() def __updateFrameStyle(self): # type: () -> None if self.isSelected(): pen = QPen(QColor(96, 158, 215), 1.25, Qt.DashDotLine) else: pen = self.__framePen self.__framePathItem.setPen(pen) def contentType(self): # type: () -> str return self.__contentType def setContent(self, content, contentType="text/plain"): # type: (str, str) -> None if self.__content != content or self.__contentType != contentType: self.__contentType = contentType self.__content = content self.__updateRenderedContent() self.contentChanged.emit() def content(self): # type: () -> str return self.__content def setPlainText(self, text): # type: (str) -> None """Set the annotation text as plain text. """ self.setContent(text, "text/plain") def toPlainText(self): # type: () -> str return self.__textItem.toPlainText() def setHtml(self, text): # type: (str) -> None """Set the annotation text as html. """ self.setContent(text, "text/html") def toHtml(self): # type: () -> str return self.__textItem.toHtml() def setDefaultTextColor(self, color): # type: (QColor) -> None """Set the default text color. """ self.__textItem.setDefaultTextColor(color) def defaultTextColor(self): # type: () -> QColor return self.__textItem.defaultTextColor() def setTextMargins(self, left, top, right, bottom): # type: (int, int, int, int) -> None """Set the text margins. """ margins = (left, top, right, bottom) if self.__textMargins != margins: self.__textMargins = margins self.__textItem.setPos(left, top) self.__textItem.setTextWidth( max(self.geometry().width() - left - right, 0)) def textMargins(self): # type: () -> Tuple[int, int, int, int] """Return the text margins. """ return self.__textMargins def document(self): # type: () -> QTextDocument """Return the QTextDocument instance used internally. """ return self.__textItem.document() def setTextCursor(self, cursor): # type: (QTextCursor) -> None self.__textItem.setTextCursor(cursor) def textCursor(self): # type: () -> QTextCursor return self.__textItem.textCursor() def setTextInteractionFlags(self, flags): # type: (Union[Qt.TextInteractionFlags, Qt.TextInteractionFlag]) -> None self.__textInteractionFlags = Qt.TextInteractionFlags(flags) def textInteractionFlags(self): # type: () -> Qt.TextInteractionFlags return Qt.TextInteractionFlags(self.__textInteractionFlags) def setDefaultStyleSheet(self, stylesheet): # type: (str) -> None self.document().setDefaultStyleSheet(stylesheet) def mouseDoubleClickEvent(self, event): # type: (QGraphicsSceneMouseEvent) -> None super().mouseDoubleClickEvent(event) if event.buttons() == Qt.LeftButton and \ self.__textInteractionFlags & Qt.TextEditable: self.startEdit() def startEdit(self): # type: () -> None """Start the annotation text edit process. """ self.__textItem.setPlainText(self.__content) self.__textItem.setTextInteractionFlags(self.__textInteractionFlags) self.__textItem.setFocus(Qt.MouseFocusReason) self.__textItem.document().contentsChanged.connect(self.textEdited) def endEdit(self): # type: () -> None """End the annotation edit. """ content = self.__textItem.toPlainText() self.__textItem.setTextInteractionFlags(self.__defaultInteractionFlags) self.__textItem.document().contentsChanged.disconnect(self.textEdited) cursor = self.__textItem.textCursor() cursor.clearSelection() self.__textItem.setTextCursor(cursor) self.__content = content self.editingFinished.emit() # Cannot change the textItem's html immediately, this method is # invoked from it. # TODO: Separate the editor from the view. QMetaObject.invokeMethod(self, "__updateRenderedContent", Qt.QueuedConnection) def __onDocumentSizeChanged(self, size): # type: (QSizeF) -> None # The size of the text document has changed. Expand the text # control rect's height if the text no longer fits inside. rect = self.geometry() _, top, _, bottom = self.textMargins() if rect.height() < (size.height() + bottom + top): rect.setHeight(size.height() + bottom + top) self.setGeometry(rect) def __updateFrame(self): # type: () -> None rect = self.geometry() rect.moveTo(0, 0) path = QPainterPath() path.addRect(rect) self.__framePathItem.setPath(path) def resizeEvent(self, event): # type: (QGraphicsSceneResizeEvent) -> None width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() super().resizeEvent(event) def __textEditingFinished(self): # type: () -> None self.endEdit() def sceneEventFilter(self, obj, event): # type: (QGraphicsItem, QEvent) -> bool if obj is self.__textItem and \ not (self.__textItem.hasFocus() and self.__textItem.textInteractionFlags() & Qt.TextEditable) and \ event.type() in {QEvent.GraphicsSceneContextMenu} and \ event.modifiers() & Qt.AltModifier: # Handle Alt + context menu events here self.contextMenuEvent(event) event.accept() return True return super().sceneEventFilter(obj, event) def changeEvent(self, event): # type: (QEvent) -> None if event.type() == QEvent.FontChange: self.__textItem.setFont(self.font()) elif event.type() == QEvent.PaletteChange: self.__textItem.setDefaultTextColor(self.palette().color( QPalette.Text)) super().changeEvent(event) @Slot() def __updateRenderedContent(self): # type: () -> None self.__textItem.setHtml( markup.render_as_rich_text(self.__content, self.__contentType)) def contextMenuEvent(self, event): # type: (QGraphicsSceneContextMenuEvent) -> None if event.modifiers() & Qt.AltModifier: menu = QMenu(event.widget()) menu.setAttribute(Qt.WA_DeleteOnClose) formatmenu = menu.addMenu("Render as") group = QActionGroup(self) def makeaction(text, parent, data=None, **kwargs): # type: (str, QObject, Any, Any) -> QAction action = QAction(text, parent, **kwargs) if data is not None: action.setData(data) return action formatactions = [ makeaction("Plain Text", group, checkable=True, toolTip=self.tr("Render contents as plain text"), data="text/plain"), makeaction("HTML", group, checkable=True, toolTip=self.tr("Render contents as HTML"), data="text/html"), makeaction("RST", group, checkable=True, toolTip=self.tr("Render contents as RST " "(reStructuredText)"), data="text/rst"), makeaction("Markdown", group, checkable=True, toolTip=self.tr("Render contents as Markdown"), data="text/markdown") ] for action in formatactions: action.setChecked(action.data() == self.__contentType.lower()) formatmenu.addAction(action) def ontriggered(action): # type: (QAction) -> None mimetype = action.data() content = self.content() self.setContent(content, mimetype) self.editingFinished.emit() menu.triggered.connect(ontriggered) menu.popup(event.screenPos()) event.accept() else: event.ignore()
class TextAnnotation(Annotation): """Text annotation item for the canvas scheme. """ editingFinished = Signal() """Emitted when the editing is finished (i.e. the item loses focus).""" textEdited = Signal() """Emitted when the edited text changes.""" def __init__(self, parent=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) self.__textMargins = (2, 2, 2, 2) rect = self.geometry().translated(-self.pos()) self.__framePen = QPen(Qt.NoPen) self.__framePathItem = QGraphicsPathItem(self) self.__framePathItem.setPen(self.__framePen) self.__textItem = GraphicsTextEdit(self) self.__textItem.setPlaceholderText(self.tr("Enter text here")) self.__textItem.setPos(2, 2) self.__textItem.setTextWidth(rect.width() - 4) self.__textItem.setTabChangesFocus(True) self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.setFont(self.font()) self.__textInteractionFlags = Qt.NoTextInteraction layout = self.__textItem.document().documentLayout() layout.documentSizeChanged.connect(self.__onDocumentSizeChanged) self.__updateFrame() def adjustSize(self): """Resize to a reasonable size. """ self.__textItem.setTextWidth(-1) self.__textItem.adjustSize() size = self.__textItem.boundingRect().size() left, top, right, bottom = self.textMargins() geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom)) self.setGeometry(geom) def setFramePen(self, pen): """Set the frame pen. By default Qt.NoPen is used (i.e. the frame is not shown). """ if pen != self.__framePen: self.__framePen = QPen(pen) self.__updateFrameStyle() def framePen(self): """Return the frame pen. """ return QPen(self.__framePen) def setFrameBrush(self, brush): """Set the frame brush. """ self.__framePathItem.setBrush(brush) def frameBrush(self): """Return the frame brush. """ return self.__framePathItem.brush() def __updateFrameStyle(self): if self.isSelected(): pen = QPen(QColor(96, 158, 215), 1.25, Qt.DashDotLine) else: pen = self.__framePen self.__framePathItem.setPen(pen) def setPlainText(self, text): """Set the annotation plain text. """ self.__textItem.setPlainText(text) def toPlainText(self): return self.__textItem.toPlainText() def setHtml(self, text): """Set the annotation rich text. """ self.__textItem.setHtml(text) def toHtml(self): return self.__textItem.toHtml() def setDefaultTextColor(self, color): """Set the default text color. """ self.__textItem.setDefaultTextColor(color) def defaultTextColor(self): return self.__textItem.defaultTextColor() def setTextMargins(self, left, top, right, bottom): """Set the text margins. """ margins = (left, top, right, bottom) if self.__textMargins != margins: self.__textMargins = margins self.__textItem.setPos(left, top) self.__textItem.setTextWidth(max(self.geometry().width() - left - right, 0)) def textMargins(self): """Return the text margins. """ return self.__textMargins def document(self): """Return the QTextDocument instance used internally. """ return self.__textItem.document() def setTextCursor(self, cursor): self.__textItem.setTextCursor(cursor) def textCursor(self): return self.__textItem.textCursor() def setTextInteractionFlags(self, flags): self.__textInteractionFlags = flags def textInteractionFlags(self): return self.__textInteractionFlags def setDefaultStyleSheet(self, stylesheet): self.document().setDefaultStyleSheet(stylesheet) def mouseDoubleClickEvent(self, event): Annotation.mouseDoubleClickEvent(self, event) if event.buttons() == Qt.LeftButton and self.__textInteractionFlags & Qt.TextEditable: self.startEdit() def startEdit(self): """Start the annotation text edit process. """ self.__textItem.setTextInteractionFlags(self.__textInteractionFlags) self.__textItem.setFocus(Qt.MouseFocusReason) # Install event filter to find out when the text item loses focus. self.__textItem.installSceneEventFilter(self) self.__textItem.document().contentsChanged.connect(self.textEdited) def endEdit(self): """End the annotation edit. """ self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.removeSceneEventFilter(self) self.__textItem.document().contentsChanged.disconnect(self.textEdited) cursor = self.__textItem.textCursor() cursor.clearSelection() self.__textItem.setTextCursor(cursor) self.editingFinished.emit() def __onDocumentSizeChanged(self, size): # The size of the text document has changed. Expand the text # control rect's height if the text no longer fits inside. try: rect = self.geometry() _, top, _, bottom = self.textMargins() if rect.height() < (size.height() + bottom + top): rect.setHeight(size.height() + bottom + top) self.setGeometry(rect) except Exception: log.error("error in __onDocumentSizeChanged", exc_info=True) def __updateFrame(self): rect = self.geometry() rect.moveTo(0, 0) path = QPainterPath() path.addRect(rect) self.__framePathItem.setPath(path) def resizeEvent(self, event): width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() QGraphicsWidget.resizeEvent(self, event) def sceneEventFilter(self, obj, event): if obj is self.__textItem and event.type() == QEvent.FocusOut: self.__textItem.focusOutEvent(event) self.endEdit() return True return Annotation.sceneEventFilter(self, obj, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateFrameStyle() return Annotation.itemChange(self, change, value) def changeEvent(self, event): if event.type() == QEvent.FontChange: self.__textItem.setFont(self.font()) Annotation.changeEvent(self, event)
class TextAnnotation(Annotation): """ Text annotation item for the canvas scheme. Text interaction (if enabled) is started by double clicking the item. """ #: Emitted when the editing is finished (i.e. the item loses edit focus). editingFinished = Signal() #: Emitted when the text content changes on user interaction. textEdited = Signal() #: Emitted when the text annotation's contents change #: (`content` or `contentType` changed) contentChanged = Signal() #: Mapping of supported content types to corresponding #: content -> html transformer. ContentRenderer = OrderedDict([ ("text/plain", render_plain), ("text/rst", render_rst), ("text/html", render_html), ]) # type: Dict[str, Callable[[str], [str]]] def __init__(self, parent=None, **kwargs): super().__init__(None, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) self.__contentType = "text/plain" self.__content = "" self.__renderer = render_plain self.__textMargins = (2, 2, 2, 2) self.__textInteractionFlags = Qt.NoTextInteraction self.__defaultInteractionFlags = ( Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) rect = self.geometry().translated(-self.pos()) self.__framePen = QPen(Qt.NoPen) self.__framePathItem = QGraphicsPathItem(self) self.__framePathItem.setPen(self.__framePen) self.__textItem = GraphicsTextEdit(self) self.__textItem.setOpenExternalLinks(True) self.__textItem.setPlaceholderText(self.tr("Enter text here")) self.__textItem.setPos(2, 2) self.__textItem.setTextWidth(rect.width() - 4) self.__textItem.setTabChangesFocus(True) self.__textItem.setTextInteractionFlags(self.__defaultInteractionFlags) self.__textItem.setFont(self.font()) self.__textItem.editingFinished.connect(self.__textEditingFinished) self.__textItem.setDefaultTextColor( self.palette().color(QPalette.Text) ) if self.__textItem.scene() is not None: self.__textItem.installSceneEventFilter(self) layout = self.__textItem.document().documentLayout() layout.documentSizeChanged.connect(self.__onDocumentSizeChanged) self.__updateFrame() # set parent item at the end in order to ensure # QGraphicsItem.ItemSceneHasChanged is delivered after initialization if parent is not None: self.setParentItem(parent) def itemChange(self, change, value): if change == QGraphicsItem.ItemSceneHasChanged: if self.__textItem.scene() is not None: self.__textItem.installSceneEventFilter(self) if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateFrameStyle() return super().itemChange(change, value) def adjustSize(self): """Resize to a reasonable size. """ self.__textItem.setTextWidth(-1) self.__textItem.adjustSize() size = self.__textItem.boundingRect().size() left, top, right, bottom = self.textMargins() geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom)) self.setGeometry(geom) def setFramePen(self, pen): """Set the frame pen. By default Qt.NoPen is used (i.e. the frame is not shown). """ if pen != self.__framePen: self.__framePen = QPen(pen) self.__updateFrameStyle() def framePen(self): """Return the frame pen. """ return QPen(self.__framePen) def setFrameBrush(self, brush): """Set the frame brush. """ self.__framePathItem.setBrush(brush) def frameBrush(self): """Return the frame brush. """ return self.__framePathItem.brush() def __updateFrameStyle(self): if self.isSelected(): pen = QPen(QColor(96, 158, 215), 1.25, Qt.DashDotLine) else: pen = self.__framePen self.__framePathItem.setPen(pen) def contentType(self): return self.__contentType def setContent(self, content, contentType="text/plain"): if self.__content != content or self.__contentType != contentType: self.__contentType = contentType self.__content = content self.__updateRenderedContent() self.contentChanged.emit() def content(self): return self.__content def setPlainText(self, text): """Set the annotation text as plain text. """ self.setContent(text, "text/plain") def toPlainText(self): return self.__textItem.toPlainText() def setHtml(self, text): """Set the annotation text as html. """ self.setContent(text, "text/html") def toHtml(self): return self.__textItem.toHtml() def setDefaultTextColor(self, color): """Set the default text color. """ self.__textItem.setDefaultTextColor(color) def defaultTextColor(self): return self.__textItem.defaultTextColor() def setTextMargins(self, left, top, right, bottom): """Set the text margins. """ margins = (left, top, right, bottom) if self.__textMargins != margins: self.__textMargins = margins self.__textItem.setPos(left, top) self.__textItem.setTextWidth( max(self.geometry().width() - left - right, 0) ) def textMargins(self): """Return the text margins. """ return self.__textMargins def document(self): """Return the QTextDocument instance used internally. """ return self.__textItem.document() def setTextCursor(self, cursor): self.__textItem.setTextCursor(cursor) def textCursor(self): return self.__textItem.textCursor() def setTextInteractionFlags(self, flags): self.__textInteractionFlags = flags def textInteractionFlags(self): return self.__textInteractionFlags def setDefaultStyleSheet(self, stylesheet): self.document().setDefaultStyleSheet(stylesheet) def mouseDoubleClickEvent(self, event): Annotation.mouseDoubleClickEvent(self, event) if event.buttons() == Qt.LeftButton and \ self.__textInteractionFlags & Qt.TextEditable: self.startEdit() def startEdit(self): """Start the annotation text edit process. """ self.__textItem.setPlainText(self.__content) self.__textItem.setTextInteractionFlags(self.__textInteractionFlags) self.__textItem.setFocus(Qt.MouseFocusReason) self.__textItem.document().contentsChanged.connect( self.textEdited ) def endEdit(self): """End the annotation edit. """ content = self.__textItem.toPlainText() self.__textItem.setTextInteractionFlags(self.__defaultInteractionFlags) self.__textItem.document().contentsChanged.disconnect( self.textEdited ) cursor = self.__textItem.textCursor() cursor.clearSelection() self.__textItem.setTextCursor(cursor) self.__content = content self.editingFinished.emit() # Cannot change the textItem's html immediately, this method is # invoked from it. # TODO: Separate the editor from the view. QMetaObject.invokeMethod( self, "__updateRenderedContent", Qt.QueuedConnection) def __onDocumentSizeChanged(self, size): # The size of the text document has changed. Expand the text # control rect's height if the text no longer fits inside. rect = self.geometry() _, top, _, bottom = self.textMargins() if rect.height() < (size.height() + bottom + top): rect.setHeight(size.height() + bottom + top) self.setGeometry(rect) def __updateFrame(self): rect = self.geometry() rect.moveTo(0, 0) path = QPainterPath() path.addRect(rect) self.__framePathItem.setPath(path) def resizeEvent(self, event): width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() QGraphicsWidget.resizeEvent(self, event) def __textEditingFinished(self): self.endEdit() def sceneEventFilter(self, obj, event): if obj is self.__textItem and \ not (self.__textItem.hasFocus() and self.__textItem.textInteractionFlags() & Qt.TextEditable) and \ event.type() in {QEvent.GraphicsSceneContextMenu} and \ event.modifiers() & Qt.AltModifier: # Handle Alt + context menu events here self.contextMenuEvent(event) event.accept() return True return super().sceneEventFilter(obj, event) def changeEvent(self, event): if event.type() == QEvent.FontChange: self.__textItem.setFont(self.font()) elif event.type() == QEvent.PaletteChange: self.__textItem.setDefaultTextColor( self.palette().color(QPalette.Text) ) Annotation.changeEvent(self, event) @Slot() def __updateRenderedContent(self): try: renderer = TextAnnotation.ContentRenderer[self.__contentType] except KeyError: renderer = render_plain self.__textItem.setHtml(renderer(self.__content)) def contextMenuEvent(self, event): if event.modifiers() & Qt.AltModifier: menu = QMenu(event.widget()) menu.setAttribute(Qt.WA_DeleteOnClose) formatmenu = menu.addMenu("Render as") group = QActionGroup(self, exclusive=True) def makeaction(text, parent, data=None, **kwargs): action = QAction(text, parent, **kwargs) if data is not None: action.setData(data) return action formatactions = [ makeaction("Plain Text", group, checkable=True, toolTip=self.tr("Render contents as plain text"), data="text/plain"), makeaction("HTML", group, checkable=True, toolTip=self.tr("Render contents as HTML"), data="text/html"), makeaction("RST", group, checkable=True, toolTip=self.tr("Render contents as RST " "(reStructuredText)"), data="text/rst"), makeaction("Markdown", group, checkable=True, toolTip=self.tr("Render contents as Markdown"), data="text/markdown") ] for action in formatactions: action.setChecked(action.data() == self.__contentType.lower()) formatmenu.addAction(action) def ontriggered(action): mimetype = action.data() content = self.content() self.setContent(content, mimetype) self.editingFinished.emit() menu.triggered.connect(ontriggered) menu.popup(event.screenPos()) event.accept() else: event.ignore()
class TextAnnotation(Annotation): """Text annotation item for the canvas scheme. """ editingFinished = Signal() """Emitted when the editing is finished (i.e. the item loses focus).""" textEdited = Signal() """Emitted when the edited text changes.""" def __init__(self, parent=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) self.__textMargins = (2, 2, 2, 2) rect = self.geometry().translated(-self.pos()) self.__framePen = QPen(Qt.NoPen) self.__framePathItem = QGraphicsPathItem(self) self.__framePathItem.setPen(self.__framePen) self.__textItem = GraphicsTextEdit(self) self.__textItem.setPlaceholderText(self.tr("Enter text here")) self.__textItem.setPos(2, 2) self.__textItem.setTextWidth(rect.width() - 4) self.__textItem.setTabChangesFocus(True) self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.setFont(self.font()) self.__textInteractionFlags = Qt.NoTextInteraction layout = self.__textItem.document().documentLayout() layout.documentSizeChanged.connect(self.__onDocumentSizeChanged) self.__updateFrame() def adjustSize(self): """Resize to a reasonable size. """ self.__textItem.setTextWidth(-1) self.__textItem.adjustSize() size = self.__textItem.boundingRect().size() left, top, right, bottom = self.textMargins() geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom)) self.setGeometry(geom) def setFramePen(self, pen): """Set the frame pen. By default Qt.NoPen is used (i.e. the frame is not shown). """ if pen != self.__framePen: self.__framePen = QPen(pen) self.__updateFrameStyle() def framePen(self): """Return the frame pen. """ return QPen(self.__framePen) def setFrameBrush(self, brush): """Set the frame brush. """ self.__framePathItem.setBrush(brush) def frameBrush(self): """Return the frame brush. """ return self.__framePathItem.brush() def __updateFrameStyle(self): if self.isSelected(): pen = QPen(QColor(96, 158, 215), 1.25, Qt.DashDotLine) else: pen = self.__framePen self.__framePathItem.setPen(pen) def setPlainText(self, text): """Set the annotation plain text. """ self.__textItem.setPlainText(text) def toPlainText(self): return self.__textItem.toPlainText() def setHtml(self, text): """Set the annotation rich text. """ self.__textItem.setHtml(text) def toHtml(self): return self.__textItem.toHtml() def setDefaultTextColor(self, color): """Set the default text color. """ self.__textItem.setDefaultTextColor(color) def defaultTextColor(self): return self.__textItem.defaultTextColor() def setTextMargins(self, left, top, right, bottom): """Set the text margins. """ margins = (left, top, right, bottom) if self.__textMargins != margins: self.__textMargins = margins self.__textItem.setPos(left, top) self.__textItem.setTextWidth( max(self.geometry().width() - left - right, 0)) def textMargins(self): """Return the text margins. """ return self.__textMargins def document(self): """Return the QTextDocument instance used internally. """ return self.__textItem.document() def setTextCursor(self, cursor): self.__textItem.setTextCursor(cursor) def textCursor(self): return self.__textItem.textCursor() def setTextInteractionFlags(self, flags): self.__textInteractionFlags = flags def textInteractionFlags(self): return self.__textInteractionFlags def setDefaultStyleSheet(self, stylesheet): self.document().setDefaultStyleSheet(stylesheet) def mouseDoubleClickEvent(self, event): Annotation.mouseDoubleClickEvent(self, event) if event.buttons() == Qt.LeftButton and \ self.__textInteractionFlags & Qt.TextEditable: self.startEdit() def startEdit(self): """Start the annotation text edit process. """ self.__textItem.setTextInteractionFlags(self.__textInteractionFlags) self.__textItem.setFocus(Qt.MouseFocusReason) # Install event filter to find out when the text item loses focus. self.__textItem.installSceneEventFilter(self) self.__textItem.document().contentsChanged.connect(self.textEdited) def endEdit(self): """End the annotation edit. """ self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.removeSceneEventFilter(self) self.__textItem.document().contentsChanged.disconnect(self.textEdited) cursor = self.__textItem.textCursor() cursor.clearSelection() self.__textItem.setTextCursor(cursor) self.editingFinished.emit() def __onDocumentSizeChanged(self, size): # The size of the text document has changed. Expand the text # control rect's height if the text no longer fits inside. try: rect = self.geometry() _, top, _, bottom = self.textMargins() if rect.height() < (size.height() + bottom + top): rect.setHeight(size.height() + bottom + top) self.setGeometry(rect) except Exception: log.error("error in __onDocumentSizeChanged", exc_info=True) def __updateFrame(self): rect = self.geometry() rect.moveTo(0, 0) path = QPainterPath() path.addRect(rect) self.__framePathItem.setPath(path) def resizeEvent(self, event): width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() QGraphicsWidget.resizeEvent(self, event) def sceneEventFilter(self, obj, event): if obj is self.__textItem and event.type() == QEvent.FocusOut: self.__textItem.focusOutEvent(event) self.endEdit() return True return Annotation.sceneEventFilter(self, obj, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateFrameStyle() return Annotation.itemChange(self, change, value) def changeEvent(self, event): if event.type() == QEvent.FontChange: self.__textItem.setFont(self.font()) Annotation.changeEvent(self, event)