class ArrowAnnotation(Annotation): def __init__(self, parent=None, line=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) if line is None: line = QLineF(0, 0, 20, 0) self.__line = QLineF(line) self.__color = QColor(Qt.red) # An item with the same shape as this arrow, stacked behind this # item as a source for QGraphicsDropShadowEffect. Cannot attach # the effect to this item directly as QGraphicsEffect makes the item # non devicePixelRatio aware. self.__arrowShadowBase = ArrowItem(self, line=line) self.__arrowShadowBase.setPen(Qt.NoPen) # no pen -> slightly thinner self.__arrowShadowBase.setBrush(QBrush(self.__color)) self.__arrowShadowBase.setArrowStyle(ArrowItem.Concave) self.__arrowShadowBase.setLineWidth(5) self.__shadow = QGraphicsDropShadowEffect( blurRadius=5, offset=QPointF(1.0, 2.0), ) self.__arrowShadowBase.setGraphicsEffect(self.__shadow) self.__shadow.setEnabled(True) # The 'real' shape item self.__arrowItem = ArrowItem(self, line=line) self.__arrowItem.setBrush(self.__color) self.__arrowItem.setPen(QPen(self.__color)) self.__arrowItem.setArrowStyle(ArrowItem.Concave) self.__arrowItem.setLineWidth(5) self.__autoAdjustGeometry = True def setAutoAdjustGeometry(self, autoAdjust): """ If set to `True` then the geometry will be adjusted whenever the arrow is changed with `setLine`. Otherwise the geometry of the item is only updated so the `line` lies within the `geometry()` rect (i.e. it only grows). True by default """ self.__autoAdjustGeometry = autoAdjust if autoAdjust: self.adjustGeometry() def autoAdjustGeometry(self): """ Should the geometry of the item be adjusted automatically when `setLine` is called. """ return self.__autoAdjustGeometry def setLine(self, line): """ Set the arrow base line (a `QLineF` in object coordinates). """ if self.__line != line: self.__line = QLineF(line) # local item coordinate system geom = self.geometry().translated(-self.pos()) if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) arrow_shape = arrow_path_concave(line, self.lineWidth()) arrow_rect = arrow_shape.boundingRect() if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) if self.__autoAdjustGeometry: # Shrink the geometry if required. geom = geom.intersected(arrow_rect) # topLeft can move changing the local coordinates. diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) self.__arrowItem.setLine(line) self.__arrowShadowBase.setLine(line) self.__line = line # parent item coordinate system geom.translate(self.pos()) self.setGeometry(geom) def line(self): """ Return the arrow base line (`QLineF` in object coordinates). """ return QLineF(self.__line) def setColor(self, color): """ Set arrow brush color. """ if self.__color != color: self.__color = QColor(color) self.__updateStyleState() def color(self): """ Return the arrow brush color. """ return QColor(self.__color) def setLineWidth(self, lineWidth): """ Set the arrow line width. """ self.__arrowItem.setLineWidth(lineWidth) self.__arrowShadowBase.setLineWidth(lineWidth) def lineWidth(self): """ Return the arrow line width. """ return self.__arrowItem.lineWidth() def adjustGeometry(self): """ Adjust the widget geometry to exactly fit the arrow inside while preserving the arrow path scene geometry. """ # local system coordinate geom = self.geometry().translated(-self.pos()) line = self.__line arrow_rect = self.__arrowItem.shape().boundingRect() if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) geom = geom.intersected(arrow_rect) diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) geom.translate(self.pos()) self.setGeometry(geom) self.setLine(line) def shape(self): arrow_shape = self.__arrowItem.shape() return self.mapFromItem(self.__arrowItem, arrow_shape) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateStyleState() return Annotation.itemChange(self, change, value) def __updateStyleState(self): """ Update the arrows' brush, pen, ... based on it's state """ if self.isSelected(): color = self.__color.darker(150) pen = QPen(QColor(96, 158, 215), Qt.DashDotLine) pen.setWidthF(1.25) pen.setCosmetic(True) shadow = pen.color().darker(150) else: color = self.__color pen = QPen(color) shadow = QColor(63, 63, 63, 180) self.__arrowShadowBase.setBrush(color) self.__shadow.setColor(shadow) self.__arrowItem.setBrush(color) self.__arrowItem.setPen(pen)
class ArrowAnnotation(Annotation): def __init__(self, parent=None, line=None, **kwargs): # type: (Optional[QGraphicsItem], Optional[QLineF], Any) -> None super().__init__(parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) if line is None: line = QLineF(0, 0, 20, 0) self.__line = QLineF(line) self.__color = QColor(Qt.red) # An item with the same shape as this arrow, stacked behind this # item as a source for QGraphicsDropShadowEffect. Cannot attach # the effect to this item directly as QGraphicsEffect makes the item # non devicePixelRatio aware. self.__arrowShadowBase = ArrowItem(self, line=line) self.__arrowShadowBase.setPen(Qt.NoPen) # no pen -> slightly thinner self.__arrowShadowBase.setBrush(QBrush(self.__color)) self.__arrowShadowBase.setArrowStyle(ArrowItem.Concave) self.__arrowShadowBase.setLineWidth(5) self.__shadow = QGraphicsDropShadowEffect( blurRadius=5, offset=QPointF(1.0, 2.0), ) self.__arrowShadowBase.setGraphicsEffect(self.__shadow) self.__shadow.setEnabled(True) # The 'real' shape item self.__arrowItem = ArrowItem(self, line=line) self.__arrowItem.setBrush(self.__color) self.__arrowItem.setPen(QPen(self.__color)) self.__arrowItem.setArrowStyle(ArrowItem.Concave) self.__arrowItem.setLineWidth(5) self.__autoAdjustGeometry = True def setAutoAdjustGeometry(self, autoAdjust): # type: (bool) -> None """ If set to `True` then the geometry will be adjusted whenever the arrow is changed with `setLine`. Otherwise the geometry of the item is only updated so the `line` lies within the `geometry()` rect (i.e. it only grows). True by default """ self.__autoAdjustGeometry = autoAdjust if autoAdjust: self.adjustGeometry() def autoAdjustGeometry(self): # type: () -> bool """ Should the geometry of the item be adjusted automatically when `setLine` is called. """ return self.__autoAdjustGeometry def setLine(self, line): # type: (QLineF) -> None """ Set the arrow base line (a `QLineF` in object coordinates). """ if self.__line != line: self.__line = QLineF(line) # local item coordinate system geom = self.geometry().translated(-self.pos()) if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) arrow_shape = arrow_path_concave(line, self.lineWidth()) arrow_rect = arrow_shape.boundingRect() if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) if self.__autoAdjustGeometry: # Shrink the geometry if required. geom = geom.intersected(arrow_rect) # topLeft can move changing the local coordinates. diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) self.__arrowItem.setLine(line) self.__arrowShadowBase.setLine(line) self.__line = line # parent item coordinate system geom.translate(self.pos()) self.setGeometry(geom) def line(self): # type: () -> QLineF """ Return the arrow base line (`QLineF` in object coordinates). """ return QLineF(self.__line) def setColor(self, color): # type: (QColor) -> None """ Set arrow brush color. """ if self.__color != color: self.__color = QColor(color) self.__updateStyleState() def color(self): # type: () -> QColor """ Return the arrow brush color. """ return QColor(self.__color) def setLineWidth(self, lineWidth): # type: (float) -> None """ Set the arrow line width. """ self.__arrowItem.setLineWidth(lineWidth) self.__arrowShadowBase.setLineWidth(lineWidth) def lineWidth(self): # type: () -> float """ Return the arrow line width. """ return self.__arrowItem.lineWidth() def adjustGeometry(self): # type: () -> None """ Adjust the widget geometry to exactly fit the arrow inside while preserving the arrow path scene geometry. """ # local system coordinate geom = self.geometry().translated(-self.pos()) line = self.__line arrow_rect = self.__arrowItem.shape().boundingRect() if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) geom = geom.intersected(arrow_rect) diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) geom.translate(self.pos()) self.setGeometry(geom) self.setLine(line) def shape(self): # type: () -> QPainterPath arrow_shape = self.__arrowItem.shape() return self.mapFromItem(self.__arrowItem, arrow_shape) def itemChange(self, change, value): # type: (QGraphicsItem.GraphicsItemChange, Any) -> Any if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateStyleState() return super().itemChange(change, value) def __updateStyleState(self): # type: () -> None """ Update the arrows' brush, pen, ... based on it's state """ if self.isSelected(): color = self.__color.darker(150) pen = QPen(QColor(96, 158, 215), Qt.DashDotLine) pen.setWidthF(1.25) pen.setCosmetic(True) shadow = pen.color().darker(150) else: color = self.__color pen = QPen(color) shadow = QColor(63, 63, 63, 180) self.__arrowShadowBase.setBrush(color) self.__shadow.setColor(shadow) self.__arrowItem.setBrush(color) self.__arrowItem.setPen(pen)
class NodeBodyItem(GraphicsPathObject): """ The central part (body) of the `NodeItem`. """ def __init__(self, parent=None): # type: (NodeItem) -> None super().__init__(parent) assert isinstance(parent, NodeItem) self.__processingState = 0 self.__progress = -1. self.__animationEnabled = False self.__isSelected = False self.__hover = False self.__shapeRect = QRectF(-10, -10, 20, 20) self.setAcceptHoverEvents(True) self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setPen(QPen(Qt.NoPen)) self.setPalette(default_palette()) self.shadow = QGraphicsDropShadowEffect( blurRadius=0, color=QColor(SHADOW_COLOR), offset=QPointF(0, 0), ) self.shadow.setEnabled(False) # An item with the same shape as this object, stacked behind this # item as a source for QGraphicsDropShadowEffect. Cannot attach # the effect to this item directly as QGraphicsEffect makes the item # non devicePixelRatio aware. shadowitem = GraphicsPathObject(self, objectName="shadow-shape-item") shadowitem.setPen(Qt.NoPen) shadowitem.setBrush(QBrush(QColor(SHADOW_COLOR).lighter())) shadowitem.setGraphicsEffect(self.shadow) shadowitem.setFlag(QGraphicsItem.ItemStacksBehindParent) self.__shadow = shadowitem self.__blurAnimation = QPropertyAnimation(self.shadow, b"blurRadius", self) self.__blurAnimation.setDuration(100) self.__blurAnimation.finished.connect(self.__on_finished) self.__pingAnimation = QPropertyAnimation(self, b"scale", self) self.__pingAnimation.setDuration(250) self.__pingAnimation.setKeyValues([(0.0, 1.0), (0.5, 1.1), (1.0, 1.0)]) # TODO: The body item should allow the setting of arbitrary painter # paths (for instance rounded rect, ...) def setShapeRect(self, rect): # type: (QRectF) -> None """ Set the item's shape `rect`. The item should be confined within this rect. """ path = QPainterPath() path.addEllipse(rect) self.setPath(path) self.__shadow.setPath(path) self.__shapeRect = rect def setPalette(self, palette): # type: (QPalette) -> None """ Set the body color palette (:class:`QPalette`). """ self.palette = QPalette(palette) self.__updateBrush() def setAnimationEnabled(self, enabled): # type: (bool) -> None """ Set the node animation enabled. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled def setProcessingState(self, state): # type: (int) -> None """ Set the processing state of the node. """ if self.__processingState != state: self.__processingState = state if not state and self.__animationEnabled: self.ping() def setProgress(self, progress): # type: (float) -> None """ Set the progress indicator state of the node. `progress` should be a number between 0 and 100. """ self.__progress = progress self.update() def ping(self): # type: () -> None """ Trigger a 'ping' animation. """ animation_restart(self.__pingAnimation) def hoverEnterEvent(self, event): self.__hover = True self.__updateShadowState() return super().hoverEnterEvent(event) def hoverLeaveEvent(self, event): self.__hover = False self.__updateShadowState() return super().hoverLeaveEvent(event) def paint(self, painter, option, widget=None): # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None """ Paint the shape and a progress meter. """ # Let the default implementation draw the shape if option.state & QStyle.State_Selected: # Prevent the default bounding rect selection indicator. option.state = QStyle.State(option.state ^ QStyle.State_Selected) super().paint(painter, option, widget) if self.__progress >= 0: # Draw the progress meter over the shape. # Set the clip to shape so the meter does not overflow the shape. painter.save() painter.setClipPath(self.shape(), Qt.ReplaceClip) color = self.palette.color(QPalette.ButtonText) pen = QPen(color, 5) painter.setPen(pen) painter.setRenderHints(QPainter.Antialiasing) span = max(1, int(self.__progress * 57.60)) painter.drawArc(self.__shapeRect, 90 * 16, -span) painter.restore() def __updateShadowState(self): # type: () -> None if self.__isSelected or self.__hover: enabled = True radius = 17 else: enabled = False radius = 0 if enabled and not self.shadow.isEnabled(): self.shadow.setEnabled(enabled) if self.__isSelected: color = QColor(SELECTED_SHADOW_COLOR) else: color = QColor(SHADOW_COLOR) self.shadow.setColor(color) if radius == self.shadow.blurRadius(): return if self.__animationEnabled: if self.__blurAnimation.state() == QPropertyAnimation.Running: self.__blurAnimation.pause() self.__blurAnimation.setStartValue(self.shadow.blurRadius()) self.__blurAnimation.setEndValue(radius) self.__blurAnimation.start() else: self.shadow.setBlurRadius(radius) def __updateBrush(self): # type: () -> None palette = self.palette if self.__isSelected: cg = QPalette.Active else: cg = QPalette.Inactive palette.setCurrentColorGroup(cg) c1 = palette.color(QPalette.Light) c2 = palette.color(QPalette.Button) grad = radial_gradient(c2, c1) self.setBrush(QBrush(grad)) # TODO: The selected state should be set using the # QStyle flags (State_Selected. State_HasFocus) def setSelected(self, selected): # type: (bool) -> None """ Set the `selected` state. .. note:: The item does not have `QGraphicsItem.ItemIsSelectable` flag. This property is instead controlled by the parent NodeItem. """ self.__isSelected = selected self.__updateShadowState() self.__updateBrush() def __on_finished(self): # type: () -> None if self.shadow.blurRadius() == 0: self.shadow.setEnabled(False)