"""
=========
Node Item
=========
"""
import typing
import string
from operator import attrgetter
from itertools import groupby
from functools import reduce
from xml.sax.saxutils import escape
from typing import Dict, Any, Optional, List, Iterable, Tuple, Union
from AnyQt.QtWidgets import (
QGraphicsItem, QGraphicsObject, QGraphicsWidget,
QGraphicsDropShadowEffect, QStyle, QApplication, QGraphicsSceneMouseEvent,
QGraphicsSceneContextMenuEvent, QStyleOptionGraphicsItem, QWidget,
QGraphicsEllipseItem
)
from AnyQt.QtGui import (
QPen, QBrush, QColor, QPalette, QIcon, QPainter, QPainterPath,
QPainterPathStroker, QConicalGradient,
QTransform)
from AnyQt.QtCore import (
Qt, QEvent, QPointF, QRectF, QRect, QSize, QElapsedTimer, QTimer,
QPropertyAnimation, QEasingCurve, QObject, QVariantAnimation,
QParallelAnimationGroup, Slot
)
from AnyQt.QtCore import pyqtSignal as Signal, pyqtProperty as Property
from .graphicspathobject import GraphicsPathObject
from .graphicstextitem import GraphicsTextItem, GraphicsTextEdit
from .utils import (
saturated, radial_gradient, linspace, qpainterpath_sub_path, clip
)
from ... import styles
from ...gui.iconengine import StyledIconEngine
from ...gui.utils import disconnected
from ...scheme.node import UserMessage
from ...registry import NAMED_COLORS, WidgetDescription, CategoryDescription, \
InputSignal, OutputSignal
from ...resources import icon_loader
from .utils import uniform_linear_layout_trunc
from ...utils import set_flag
if typing.TYPE_CHECKING:
from ...registry import WidgetDescription
def create_palette(light_color, color):
# type: (QColor, QColor) -> QPalette
"""
Return a new :class:`QPalette` from for the :class:`NodeBodyItem`.
"""
palette = QPalette()
palette.setColor(QPalette.Inactive, QPalette.Light,
saturated(light_color, 50))
palette.setColor(QPalette.Inactive, QPalette.Midlight,
saturated(light_color, 90))
palette.setColor(QPalette.Inactive, QPalette.Button,
light_color)
palette.setColor(QPalette.Active, QPalette.Light,
saturated(color, 50))
palette.setColor(QPalette.Active, QPalette.Midlight,
saturated(color, 90))
palette.setColor(QPalette.Active, QPalette.Button,
color)
palette.setColor(QPalette.ButtonText, QColor("#515151"))
return palette
def default_palette():
# type: () -> QPalette
"""
Create and return a default palette for a node.
"""
return create_palette(QColor(NAMED_COLORS["light-yellow"]),
QColor(NAMED_COLORS["yellow"]))
def animation_restart(animation):
# type: (QPropertyAnimation) -> None
if animation.state() == QPropertyAnimation.Running:
animation.pause()
animation.start()
SHADOW_COLOR = "#9CACB4"
SELECTED_SHADOW_COLOR = "#609ED7"
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.__spinnerValue = 0
self.__animationEnabled = False
self.__isSelected = False
self.__hover = False
self.__shapeRect = QRectF(-10, -10, 20, 20)
self.palette = QPalette()
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, duration=100
)
self.__blurAnimation.finished.connect(self.__on_finished)
self.__pingAnimation = QPropertyAnimation(
self, b"scale", self, duration=250
)
self.__pingAnimation.setKeyValues([(0.0, 1.0), (0.5, 1.1), (1.0, 1.0)])
self.__spinnerAnimation = QVariantAnimation(
self, startValue=0, endValue=360, duration=2000, loopCount=-1,
)
self.__spinnerAnimation.valueChanged.connect(self.update)
self.__spinnerStartTimer = QTimer(
self, interval=3000, singleShot=True,
timeout=self.__progressTimeout
)
# 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
self.stopSpinner()
if not state and self.__animationEnabled:
self.ping()
if state:
self.__spinnerStartTimer.start()
else:
self.__spinnerStartTimer.stop()
def setProgress(self, progress):
# type: (float) -> None
"""
Set the progress indicator state of the node. `progress` should
be a number between 0 and 100.
"""
if self.__progress != progress:
self.__progress = progress
if self.__progress >= 0:
self.stopSpinner()
self.update()
self.__spinnerStartTimer.start()
def ping(self):
# type: () -> None
"""
Trigger a 'ping' animation.
"""
animation_restart(self.__pingAnimation)
def startSpinner(self):
self.__spinnerAnimation.start()
self.__spinnerStartTimer.stop()
self.update()
def stopSpinner(self):
self.__spinnerAnimation.stop()
self.__spinnerStartTimer.stop()
self.update()
def __progressTimeout(self):
if self.__processingState:
self.startSpinner()
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 or self.__processingState \
or self.__spinnerAnimation.state() == QVariantAnimation.Running:
# Draw the progress meter over the shape.
# Set the clip to shape so the meter does not overflow the shape.
rect = self.__shapeRect
painter.save()
painter.setClipPath(self.shape(), Qt.ReplaceClip)
color = self.palette.color(QPalette.ButtonText)
pen = QPen(color, 5)
painter.setPen(pen)
spinner = self.__spinnerAnimation
indeterminate = spinner.state() != QVariantAnimation.Stopped
if indeterminate:
draw_spinner(painter, rect, 5, color,
self.__spinnerAnimation.currentValue())
else:
span = max(1, int(360 * self.__progress / 100))
draw_progress(painter, rect, 5, color, 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 self.__animationEnabled:
if self.__blurAnimation.state() == QPropertyAnimation.Running:
self.__blurAnimation.stop()
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)
class LinkAnchorIndicator(QGraphicsEllipseItem):
"""
A visual indicator of the link anchor point at both ends
of the :class:`LinkItem`.
"""
def __init__(self, parent=None):
# type: (Optional[QGraphicsItem]) -> None
self.__styleState = QStyle.State(0)
self.__linkState = LinkItem.NoState
super().__init__(parent)
self.setAcceptedMouseButtons(Qt.NoButton)
self.setRect(-3.5, -3.5, 7., 7.)
self.setPen(QPen(Qt.NoPen))
self.setBrush(QBrush(QColor("#9CACB4")))
self.hoverBrush = QBrush(QColor("#959595"))
self.__hover = False
def setHoverState(self, state):
# type: (bool) -> None
"""
The hover state is set by the LinkItem.
"""
state = set_flag(self.__styleState, QStyle.State_MouseOver, state)
self.setStyleState(state)
def setStyleState(self, state: QStyle.State):
if self.__styleState != state:
self.__styleState = state
self.update()
def setLinkState(self, state: 'LinkItem.State'):
if self.__linkState != state:
self.__linkState = state
self.update()
def paint(self, painter, option, widget=None):
# type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None
hover = self.__styleState & (QStyle.State_Selected | QStyle.State_MouseOver)
brush = self.hoverBrush if hover else self.brush()
if self.__linkState & (LinkItem.Pending | LinkItem.Invalidated):
brush = QBrush(Qt.red)
painter.setBrush(brush)
painter.setPen(self.pen())
painter.drawEllipse(self.rect())
def draw_spinner(painter, rect, penwidth, color, angle):
# type: (QPainter, QRectF, int, QColor, int) -> None
gradient = QConicalGradient()
color2 = QColor(color)
color2.setAlpha(0)
stops = [
(0.0, color),
(1.0, color2),
]
gradient.setStops(stops)
gradient.setCoordinateMode(QConicalGradient.ObjectBoundingMode)
gradient.setCenter(0.5, 0.5)
gradient.setAngle(-angle)
pen = QPen()
pen.setCapStyle(Qt.RoundCap)
pen.setWidthF(penwidth)
pen.setBrush(gradient)
painter.setPen(pen)
painter.drawEllipse(rect)
def draw_progress(painter, rect, penwidth, color, angle):
# type: (QPainter, QRectF, int, QColor, int) -> None
painter.setPen(QPen(color, penwidth))
painter.drawArc(rect, 90 * 16, -angle * 16)
[docs]class AnchorPoint(QGraphicsObject):
"""
A anchor indicator on the :class:`NodeAnchorItem`.
"""
#: Signal emitted when the item's scene position changes.
scenePositionChanged = Signal(QPointF)
#: Signal emitted when the item's `anchorDirection` changes.
anchorDirectionChanged = Signal(QPointF)
#: Signal emitted when anchor's Input/Output channel changes.
signalChanged = Signal(QGraphicsObject)
def __init__(
self,
parent: Optional[QGraphicsItem] = None,
signal: Union[InputSignal, OutputSignal, None] = None,
**kwargs
) -> None:
super().__init__(parent, **kwargs)
self.setFlag(QGraphicsItem.ItemIsFocusable)
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
self.setFlag(QGraphicsItem.ItemHasNoContents, True)
self.indicator = LinkAnchorIndicator(self)
self.signal = signal
self.__direction = QPointF()
self.anim = QPropertyAnimation(self, b'pos', self)
self.anim.setDuration(50)
def setSignal(self, signal):
if self.signal != signal:
self.signal = signal
self.signalChanged.emit(self)
[docs] def anchorScenePos(self):
# type: () -> QPointF
"""
Return anchor position in scene coordinates.
"""
return self.mapToScene(QPointF(0, 0))
[docs] def setAnchorDirection(self, direction):
# type: (QPointF) -> None
"""
Set the preferred direction (QPointF) in item coordinates.
"""
if self.__direction != direction:
self.__direction = QPointF(direction)
self.anchorDirectionChanged.emit(direction)
[docs] def anchorDirection(self):
# type: () -> QPointF
"""
Return the preferred anchor direction.
"""
return QPointF(self.__direction)
[docs] def itemChange(self, change, value):
# type: (QGraphicsItem.GraphicsItemChange, Any) -> Any
if change == QGraphicsItem.ItemScenePositionHasChanged:
self.scenePositionChanged.emit(value)
return super().itemChange(change, value)
[docs] def boundingRect(self,):
# type: () -> QRectF
return QRectF()
def setHoverState(self, enabled):
self.indicator.setHoverState(enabled)
def setLinkState(self, state: 'LinkItem.State'):
self.indicator.setLinkState(state)
ANCHOR_TEXT_MARGIN = 4
def make_channel_anchors_path(
path: QPainterPath, #: The full uninterrupted anchor path
anchors: int, #: Number of anchors
spacing=2. #: Spacing
) -> QPainterPath:
"""Create a subdivided channel anchors path."""
if path.isEmpty() or anchors <= 1:
return QPainterPath(path)
pathlen = path.length()
spacing = min(spacing, pathlen / anchors)
delta = (spacing / 2) / pathlen # half of inner spacing
splits = list(linspace(anchors + 1))
# adjust linspace splits to give all sub paths equal length (inner
# paths get `delta` subtracted twice while edge paths only once)
splits = [(1 + 2 * delta) * x - delta for x in splits]
splits = splits[1:-1]
start = 0.0
subpaths = []
for p in splits:
subpaths.append(qpainterpath_sub_path(path, start, p - delta))
start = p + delta
subpaths.append(qpainterpath_sub_path(path, start, 1.0))
return reduce(QPainterPath.united, subpaths, QPainterPath())
class NodeAnchorItem(GraphicsPathObject):
"""
The left/right widget input/output anchors.
"""
def __init__(self, parent, **kwargs):
# type: (Optional[QGraphicsItem], Any) -> None
super().__init__(parent, **kwargs)
self.__parentNodeItem = None # type: Optional[NodeItem]
self.setAcceptHoverEvents(True)
self.setPen(QPen(Qt.NoPen))
self.normalBrush = QBrush(QColor("#CDD5D9"))
self.normalHoverBrush = QBrush(QColor("#9CACB4"))
self.connectedBrush = self.normalHoverBrush
self.connectedHoverBrush = QBrush(QColor("#959595"))
self.setBrush(self.normalBrush)
self.__animationEnabled = False
self.__hover = False
self.__anchorOpen = False
self.__compatibleSignals = None
self.__keepSignalsOpen = []
# Does this item have any anchored links.
self.anchored = False
if isinstance(parent, NodeItem):
self.__parentNodeItem = parent
else:
self.__parentNodeItem = None
self.__anchorPath = QPainterPath()
self.__points = [] # type: List[AnchorPoint]
self.__uniformPointPositions = [] # type: List[float]
self.__channelPointPositions = [] # type: List[float]
self.__incompatible = False # type: bool
self.__signals = [] # type: List[Union[InputSignal, OutputSignal]]
self.__signalLabels = [] # type: List[GraphicsTextItem]
self.__signalLabelAnims = [] # type: List[QPropertyAnimation]
self.__fullStroke = QPainterPath()
self.__dottedStroke = QPainterPath()
self.__channelStroke = QPainterPath()
self.__shape = None # type: Optional[QPainterPath]
self.shadow = QGraphicsDropShadowEffect(
blurRadius=0,
color=QColor(SHADOW_COLOR),
offset=QPointF(0, 0),
)
# self.setGraphicsEffect(self.shadow)
self.shadow.setEnabled(False)
shadowitem = GraphicsPathObject(self, objectName="shadow-shape-item")
shadowitem.setPen(Qt.NoPen)
shadowitem.setBrush(QBrush(QColor(SHADOW_COLOR)))
shadowitem.setGraphicsEffect(self.shadow)
shadowitem.setFlag(QGraphicsItem.ItemStacksBehindParent)
self.__shadow = shadowitem
self.__blurAnimation = QPropertyAnimation(self.shadow, b"blurRadius",
self)
self.__blurAnimation.setDuration(50)
self.__blurAnimation.finished.connect(self.__on_finished)
self.animGroup = QParallelAnimationGroup()
def setSignals(self, signals):
self.__signals = signals
self.setAnchorPath(self.__anchorPath) # (re)instantiate anchor paths
# TODO this is ugly
alignLeft = isinstance(self, SourceAnchorItem)
for s in signals:
lbl = GraphicsTextItem(self)
lbl.setAcceptedMouseButtons(Qt.NoButton)
lbl.setAcceptHoverEvents(False)
text = s.name
lbl.setHtml('<div align="' + ('left' if alignLeft else 'right') +
'" style="font-size: small; background-color: palette(base);" >{0}</div>'
.format(text))
cperc = self.__getChannelPercent(s)
sigPos = self.__anchorPath.pointAtPercent(cperc)
lblrect = lbl.boundingRect()
transform = QTransform()
transform.translate(sigPos.x(), sigPos.y())
transform.translate(0, -lblrect.height() / 2)
if not alignLeft:
transform.translate(-lblrect.width() - ANCHOR_TEXT_MARGIN, 0)
else:
transform.translate(ANCHOR_TEXT_MARGIN, 0)
lbl.setTransform(transform)
lbl.setOpacity(0)
self.__signalLabels.append(lbl)
lblAnim = QPropertyAnimation(lbl, b'opacity', self)
lblAnim.setDuration(50)
self.animGroup.addAnimation(lblAnim)
self.__signalLabelAnims.append(lblAnim)
def setIncompatible(self, enabled):
if self.__incompatible != enabled:
self.__incompatible = enabled
self.__updatePositions()
def setKeepAnchorOpen(self, signal):
if signal is None:
self.__keepSignalsOpen = []
elif not isinstance(signal, list):
self.__keepSignalsOpen = [signal]
else:
self.__keepSignalsOpen = signal
self.__updateLabels(self.__keepSignalsOpen)
self.__updatePositions()
def parentNodeItem(self):
# type: () -> Optional['NodeItem']
"""
Return a parent :class:`NodeItem` or ``None`` if this anchor's
parent is not a :class:`NodeItem` instance.
"""
return self.__parentNodeItem
def setAnchorPath(self, path):
# type: (QPainterPath) -> None
"""
Set the anchor's curve path as a :class:`QPainterPath`.
"""
self.__anchorPath = QPainterPath(path)
# Create a stroke of the path.
stroke_path = QPainterPathStroker()
stroke_path.setCapStyle(Qt.RoundCap)
# Shape is wider (bigger mouse hit area - should be settable)
stroke_path.setWidth(25)
self.prepareGeometryChange()
self.__shape = stroke_path.createStroke(path)
stroke_width = 3
stroke_path.setWidth(stroke_width)
# The full stroke
self.__fullStroke = stroke_path.createStroke(path)
# The dotted stroke (when not connected to anything)
self.__dottedStroke = stroke_path.createStroke(
make_channel_anchors_path(path, 6, spacing=stroke_width + 4)
)
# The channel stroke (when channels are open)
self.__channelStroke = stroke_path.createStroke(
make_channel_anchors_path(
path, len(self.__signals), spacing=stroke_width + 4
))
if self.anchored:
self.setPath(self.__fullStroke)
self.__shadow.setPath(self.__fullStroke)
brush = self.connectedHoverBrush if self.__hover else self.connectedBrush
self.setBrush(brush)
else:
self.setPath(self.__dottedStroke)
self.__shadow.setPath(self.__dottedStroke)
brush = self.normalHoverBrush if self.__hover else self.normalBrush
self.setBrush(brush)
def anchorPath(self):
# type: () -> QPainterPath
"""
Return the anchor path (:class:`QPainterPath`). This is a curve on
which the anchor points lie.
"""
return QPainterPath(self.__anchorPath)
def setAnchored(self, anchored):
# type: (bool) -> None
"""
Set the items anchored state. When ``False`` the item draws it self
with a dotted stroke.
"""
self.anchored = anchored
if anchored:
self.shadow.setEnabled(False)
self.setBrush(self.connectedBrush)
else:
brush = self.normalHoverBrush if self.__hover else self.normalBrush
self.setBrush(brush)
self.__updatePositions()
def setConnectionHint(self, hint=None):
"""
Set the connection hint. This can be used to indicate if
a connection can be made or not.
"""
raise NotImplementedError
def count(self):
# type: () -> int
"""
Return the number of anchor points.
"""
return len(self.__points)
def addAnchor(self, anchor):
# type: (AnchorPoint) -> int
"""
Add a new :class:`AnchorPoint` to this item and return it's index.
The `position` specifies where along the `anchorPath` is the new
point inserted.
"""
return self.insertAnchor(self.count(), anchor)
def __updateAnchorSignalPosition(self, anchor):
cperc = self.__getChannelPercent(anchor.signal)
i = self.__points.index(anchor)
self.__channelPointPositions[i] = cperc
self.__updatePositions()
def insertAnchor(self, index, anchor):
# type: (int, AnchorPoint) -> int
"""
Insert a new :class:`AnchorPoint` at `index`.
See also
--------
NodeAnchorItem.addAnchor
"""
if anchor in self.__points:
raise ValueError("%s already added." % anchor)
self.__points.insert(index, anchor)
self.__uniformPointPositions.insert(index, 0)
cperc = self.__getChannelPercent(anchor.signal)
self.__channelPointPositions.insert(index, cperc)
self.animGroup.addAnimation(anchor.anim)
anchor.setParentItem(self)
anchor.destroyed.connect(self.__onAnchorDestroyed)
anchor.signalChanged.connect(self.__updateAnchorSignalPosition)
positions = self.anchorPositions()
positions = uniform_linear_layout_trunc(positions)
if anchor.signal in self.__keepSignalsOpen or \
self.__anchorOpen and self.__hover:
perc = cperc
else:
perc = positions[index]
pos = self.__anchorPath.pointAtPercent(perc)
anchor.setPos(pos)
self.setAnchorPositions(positions)
self.setAnchored(bool(self.__points))
hover_for_color = self.__hover and len(self.__points) > 1 # a stylistic choice
anchor.setHoverState(hover_for_color)
return index
def removeAnchor(self, anchor):
# type: (AnchorPoint) -> None
"""
Remove and delete the anchor point.
"""
anchor = self.takeAnchor(anchor)
self.animGroup.removeAnimation(anchor.anim)
anchor.hide()
anchor.setParentItem(None)
anchor.deleteLater()
positions = self.anchorPositions()
positions = uniform_linear_layout_trunc(positions)
self.setAnchorPositions(positions)
def takeAnchor(self, anchor):
# type: (AnchorPoint) -> AnchorPoint
"""
Remove the anchor but don't delete it.
"""
index = self.__points.index(anchor)
del self.__points[index]
del self.__uniformPointPositions[index]
del self.__channelPointPositions[index]
anchor.destroyed.disconnect(self.__onAnchorDestroyed)
self.__updatePositions()
self.setAnchored(bool(self.__points))
return anchor
def __onAnchorDestroyed(self, anchor):
# type: (QObject) -> None
try:
index = self.__points.index(anchor)
except ValueError:
return
del self.__points[index]
del self.__uniformPointPositions[index]
del self.__channelPointPositions[index]
def anchorPoints(self):
# type: () -> List[AnchorPoint]
"""
Return a list of anchor points.
"""
return list(self.__points)
def anchorPoint(self, index):
# type: (int) -> AnchorPoint
"""
Return the anchor point at `index`.
"""
return self.__points[index]
def setAnchorPositions(self, positions):
# type: (Iterable[float]) -> None
"""
Set the anchor positions in percentages (0..1) along the path curve.
"""
if self.__uniformPointPositions != positions:
self.__uniformPointPositions = list(positions)
self.__updatePositions()
def anchorPositions(self):
# type: () -> List[float]
"""
Return the positions of anchor points as a list of floats where
each float is between 0 and 1 and specifies where along the anchor
path does the point lie (0 is at start 1 is at the end).
"""
return list(self.__uniformPointPositions)
def shape(self):
# type: () -> QPainterPath
if self.__shape is not None:
return QPainterPath(self.__shape)
else:
return super().shape()
def boundingRect(self):
if self.__shape is not None:
return self.__shape.controlPointRect()
else:
return GraphicsPathObject.boundingRect(self)
def setHovered(self, enabled):
self.__hover = enabled
if enabled:
brush = self.connectedHoverBrush if self.anchored else self.normalHoverBrush
else:
brush = self.connectedBrush if self.anchored else self.normalBrush
self.setBrush(brush)
self.__updateHoverState()
def hoverEnterEvent(self, event):
self.setHovered(True)
return super().hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setHovered(False)
return super().hoverLeaveEvent(event)
def setAnimationEnabled(self, enabled):
# type: (bool) -> None
"""
Set the anchor animation enabled.
"""
if self.__animationEnabled != enabled:
self.__animationEnabled = enabled
def signalAtPos(self, scenePos, signalsToFind=None):
if signalsToFind is None:
signalsToFind = self.__signals
pos = self.mapFromScene(scenePos)
def signalLengthToPos(s):
perc = self.__getChannelPercent(s)
p = self.__anchorPath.pointAtPercent(perc)
return (p - pos).manhattanLength()
return min(signalsToFind, key=signalLengthToPos)
def __updateHoverState(self):
self.__updateShadowState()
self.__updatePositions()
for indicator in self.anchorPoints():
indicator.setHoverState(self.__hover)
def __getChannelPercent(self, signal):
if signal is None:
return 0.5
signals = self.__signals
ci = signals.index(signal)
gap_perc = 1 / 8
seg_perc = (1 - (gap_perc * (len(signals) - 1))) / len(signals)
return clip((ci * (gap_perc + seg_perc)) + seg_perc / 2, 0.0, 1.0)
def __updateShadowState(self):
# type: () -> None
radius = 5 if self.__hover else 0
if radius != 0 and not self.shadow.isEnabled():
self.shadow.setEnabled(True)
if self.__animationEnabled:
if self.__blurAnimation.state() == QPropertyAnimation.Running:
self.__blurAnimation.stop()
self.__blurAnimation.setStartValue(self.shadow.blurRadius())
self.__blurAnimation.setEndValue(radius)
self.__blurAnimation.start()
else:
self.shadow.setBlurRadius(radius)
def setAnchorOpen(self, anchorOpen: bool):
"""
Should the anchors expand to expose individual channel connections.
"""
self.__anchorOpen = anchorOpen
self.__updatePositions()
def anchorOpen(self) -> bool:
return self.__anchorOpen
anchorOpen_ = Property(bool, anchorOpen, setAnchorOpen)
def setCompatibleSignals(self, compatibleSignals):
self.__compatibleSignals = compatibleSignals
self.__updatePositions()
def __updateLabels(self, showSignals):
for signal, label in zip(self.__signals, self.__signalLabels):
if signal not in showSignals:
opacity = 0
elif self.__compatibleSignals is not None \
and signal not in self.__compatibleSignals:
opacity = 0.65
else:
opacity = 1
label.setOpacity(opacity)
def __initializeAnimation(self, targetPoss, showSignals):
# TODO if animation currently running, set start value/time accordingly
for a, t in zip(self.__points, targetPoss):
currPos = a.pos()
a.anim.setStartValue(currPos)
pos = self.__anchorPath.pointAtPercent(t)
a.anim.setEndValue(pos)
for sig, lbl, lblAnim in zip(self.__signals, self.__signalLabels, self.__signalLabelAnims):
lblAnim.setStartValue(lbl.opacity())
lblAnim.setEndValue(1 if sig in showSignals else 0)
def __updatePositions(self):
# type: () -> None
"""Update anchor points positions.
"""
if self.__keepSignalsOpen or self.__anchorOpen and self.__hover:
stroke = self.__channelStroke
targetPoss = self.__channelPointPositions
showSignals = self.__keepSignalsOpen or self.__signals
elif self.anchored:
stroke = self.__fullStroke
targetPoss = self.__uniformPointPositions
showSignals = self.__signals if self.__incompatible else []
else:
stroke = self.__dottedStroke
targetPoss = self.__uniformPointPositions
showSignals = self.__signals if self.__incompatible else []
if self.animGroup.state() == QPropertyAnimation.Running:
self.animGroup.stop()
if self.__animationEnabled:
self.__initializeAnimation(targetPoss, showSignals)
self.animGroup.start()
self.setPath(stroke)
self.__shadow.setPath(stroke)
else:
for point, t in zip(self.__points, targetPoss):
pos = self.__anchorPath.pointAtPercent(t)
point.setPos(pos)
self.__updateLabels(showSignals)
self.setPath(stroke)
self.__shadow.setPath(stroke)
def __on_finished(self):
# type: () -> None
if self.shadow.blurRadius() == 0:
self.shadow.setEnabled(False)
class SourceAnchorItem(NodeAnchorItem):
"""
A source anchor item
"""
pass
class SinkAnchorItem(NodeAnchorItem):
"""
A sink anchor item.
"""
pass
def standard_icon(standard_pixmap):
# type: (QStyle.StandardPixmap) -> QIcon
"""
Return return the application style's standard icon for a
`QStyle.StandardPixmap`.
"""
style = QApplication.instance().style()
return style.standardIcon(standard_pixmap)
class GraphicsIconItem(QGraphicsWidget):
"""
A graphics item displaying an :class:`QIcon`.
"""
def __init__(self, parent=None, icon=QIcon(), iconSize=QSize(), **kwargs):
# type: (Optional[QGraphicsItem], QIcon, QSize, Any) -> None
super().__init__(parent, **kwargs)
self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption, True)
if icon is None:
icon = QIcon()
if iconSize is None or iconSize.isNull():
style = QApplication.instance().style()
size = style.pixelMetric(style.PM_LargeIconSize)
iconSize = QSize(size, size)
self.__transformationMode = Qt.SmoothTransformation
self.__iconSize = QSize(iconSize)
self.__icon = QIcon(icon)
self.anim = QPropertyAnimation(self, b"opacity")
self.anim.setDuration(350)
self.anim.setStartValue(1)
self.anim.setKeyValueAt(0.5, 0)
self.anim.setEndValue(1)
self.anim.setEasingCurve(QEasingCurve.OutQuad)
self.anim.setLoopCount(5)
def setIcon(self, icon):
# type: (QIcon) -> None
"""
Set the icon (:class:`QIcon`).
"""
if self.__icon != icon:
self.__icon = QIcon(icon)
self.update()
def icon(self):
# type: () -> QIcon
"""
Return the icon (:class:`QIcon`).
"""
return QIcon(self.__icon)
def setIconSize(self, size):
# type: (QSize) -> None
"""
Set the icon (and this item's) size (:class:`QSize`).
"""
if self.__iconSize != size:
self.prepareGeometryChange()
self.__iconSize = QSize(size)
self.update()
def iconSize(self):
# type: () -> QSize
"""
Return the icon size (:class:`QSize`).
"""
return QSize(self.__iconSize)
def setTransformationMode(self, mode):
# type: (Qt.TransformationMode) -> None
"""
Set pixmap transformation mode. (`Qt.SmoothTransformation` or
`Qt.FastTransformation`).
"""
if self.__transformationMode != mode:
self.__transformationMode = mode
self.update()
def transformationMode(self):
# type: () -> Qt.TransformationMode
"""
Return the pixmap transformation mode.
"""
return self.__transformationMode
def boundingRect(self):
# type: () -> QRectF
return QRectF(0, 0, self.__iconSize.width(), self.__iconSize.height())
def paint(self, painter, option, widget=None):
# type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None
if not self.__icon.isNull():
if option.state & QStyle.State_Selected:
mode = QIcon.Selected
elif option.state & QStyle.State_Enabled:
mode = QIcon.Normal
elif option.state & QStyle.State_Active:
mode = QIcon.Active
else:
mode = QIcon.Disabled
w, h = self.__iconSize.width(), self.__iconSize.height()
target = QRect(0, 0, w, h)
painter.setRenderHint(
QPainter.SmoothPixmapTransform,
self.__transformationMode == Qt.SmoothTransformation
)
palette = self.palette()
# assuming light-ish background color
with StyledIconEngine.setOverridePalette(palette):
self.__icon.paint(painter, target, Qt.AlignCenter, mode)
[docs]class NodeItem(QGraphicsWidget):
"""
An widget node item in the canvas.
"""
#: Signal emitted when the scene position of the node has changed.
positionChanged = Signal()
#: Signal emitted when the geometry of the channel anchors changes.
anchorGeometryChanged = Signal()
#: Signal emitted when the item has been activated (by a mouse double
#: click or a keyboard)
activated = Signal()
#: The item is under the mouse.
hovered = Signal()
#: Signal emitted the the item's selection state changes.
selectedChanged = Signal(bool)
#: Span of the anchor in degrees
ANCHOR_SPAN_ANGLE = 90
#: Z value of the item
Z_VALUE = 100
def __init__(self, widget_description=None, parent=None, **kwargs):
# type: (WidgetDescription, QGraphicsItem, Any) -> None
self.__boundingRect = None # type: Optional[QRectF]
super().__init__(parent, **kwargs)
self.setFocusPolicy(Qt.ClickFocus)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.mousePressTime = QElapsedTimer()
self.mousePressTime.start()
self.__title = ""
self.__processingState = 0
self.__progress = -1.
self.__statusMessage = ""
self.__renderedText = ""
self.__error = None # type: Optional[str]
self.__warning = None # type: Optional[str]
self.__info = None # type: Optional[str]
self.__messages = {} # type: Dict[Any, UserMessage]
self.__anchorLayout = None
self.__animationEnabled = False
self.setZValue(self.Z_VALUE)
shape_rect = QRectF(-24, -24, 48, 48)
self.shapeItem = NodeBodyItem(self)
self.shapeItem.setShapeRect(shape_rect)
self.shapeItem.setAnimationEnabled(self.__animationEnabled)
# Rect for widget's 'ears'.
anchor_rect = QRectF(-31, -31, 62, 62)
self.inputAnchorItem = SinkAnchorItem(self)
input_path = QPainterPath()
start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2
input_path.arcMoveTo(anchor_rect, start_angle)
input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE)
self.inputAnchorItem.setAnchorPath(input_path)
self.inputAnchorItem.setAnimationEnabled(self.__animationEnabled)
self.outputAnchorItem = SourceAnchorItem(self)
output_path = QPainterPath()
start_angle = self.ANCHOR_SPAN_ANGLE / 2
output_path.arcMoveTo(anchor_rect, start_angle)
output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE)
self.outputAnchorItem.setAnchorPath(output_path)
self.outputAnchorItem.setAnimationEnabled(self.__animationEnabled)
self.inputAnchorItem.hide()
self.outputAnchorItem.hide()
# Title caption item
self.captionTextItem = GraphicsTextEdit(
self, editTriggers=GraphicsTextEdit.NoEditTriggers,
returnKeyEndsEditing=True,
)
self.captionTextItem.setTabChangesFocus(True)
self.captionTextItem.setPlainText("")
self.captionTextItem.setPos(0, 33)
def iconItem(standard_pixmap):
# type: (QStyle.StandardPixmap) -> GraphicsIconItem
item = GraphicsIconItem(
self,
icon=standard_icon(standard_pixmap),
iconSize=QSize(16, 16)
)
item.hide()
return item
self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)
self.prepareGeometryChange()
self.__boundingRect = None
if widget_description is not None:
self.setWidgetDescription(widget_description)
@classmethod
def from_node(cls, node):
"""
Create an :class:`NodeItem` instance and initialize it from a
:class:`SchemeNode` instance.
"""
self = cls()
self.setWidgetDescription(node.description)
# self.setCategoryDescription(node.category)
return self
@classmethod
def from_node_meta(cls, meta_description):
"""
Create an `NodeItem` instance from a node meta description.
"""
self = cls()
self.setWidgetDescription(meta_description)
return self
# TODO: Remove the set[Widget|Category]Description. The user should
# handle setting of icons, title, ...
[docs] def setIcon(self, icon):
# type: (QIcon) -> None
"""
Set the node item's icon (:class:`QIcon`).
"""
self.icon_item = GraphicsIconItem(
self.shapeItem, icon=icon, iconSize=QSize(36, 36)
)
# assuming light-ish color background
self.icon_item.setPalette(styles.breeze_light())
self.icon_item.setPos(-18, -18)
[docs] def setColor(self, color, selectedColor=None):
# type: (QColor, Optional[QColor]) -> None
"""
Set the widget color.
"""
if selectedColor is None:
selectedColor = saturated(color, 150)
palette = create_palette(color, selectedColor)
self.shapeItem.setPalette(palette)
[docs] def setTitle(self, title):
# type: (str) -> None
"""
Set the node title. The title text is displayed at the bottom of the
node.
"""
if self.__title != title:
self.__title = title
if self.captionTextItem.isEditing():
self.captionTextItem.setPlainText(title)
else:
self.__updateTitleText()
[docs] def title(self):
# type: () -> str
"""
Return the node title.
"""
return self.__title
title_ = Property(str, fget=title, fset=setTitle,
doc="Node title text.")
#: Title editing has started
titleEditingStarted = Signal()
#: Title editing has finished
titleEditingFinished = Signal()
[docs] def editTitle(self):
"""
Start the inline title text edit process.
"""
if self.captionTextItem.isEditing():
return
self.captionTextItem.setPlainText(self.__title)
self.captionTextItem.selectAll()
self.captionTextItem.setAlignment(Qt.AlignCenter)
self.captionTextItem.document().clearUndoRedoStacks()
self.captionTextItem.editingFinished.connect(self.__editTitleFinish)
self.captionTextItem.edit()
doc = self.captionTextItem.document()
doc.documentLayout().documentSizeChanged.connect(
self.__autoLayoutTitleText, Qt.UniqueConnection
)
self.titleEditingStarted.emit()
def __editTitleFinish(self):
# called when title editing has finished
self.captionTextItem.editingFinished.disconnect(self.__editTitleFinish)
doc = self.captionTextItem.document()
doc.documentLayout().documentSizeChanged.disconnect(
self.__autoLayoutTitleText
)
name = self.captionTextItem.toPlainText()
if name != self.__title:
self.setTitle(name)
self.__updateTitleText()
self.titleEditingFinished.emit()
@Slot()
def __autoLayoutTitleText(self):
# auto layout the title during editing
doc = self.captionTextItem.document()
doc_copy = doc.clone()
doc_copy.adjustSize()
width = doc_copy.textWidth()
doc_copy.deleteLater()
if width == doc.textWidth():
return
self.prepareGeometryChange()
self.__boundingRect = None
with disconnected(
doc.documentLayout().documentSizeChanged,
self.__autoLayoutTitleText
):
doc.adjustSize()
width = self.captionTextItem.textWidth()
self.captionTextItem.setPos(-width / 2.0, 33)
[docs] def setAnimationEnabled(self, enabled):
# type: (bool) -> None
"""
Set the node animation enabled state.
"""
if self.__animationEnabled != enabled:
self.__animationEnabled = enabled
self.shapeItem.setAnimationEnabled(enabled)
self.outputAnchorItem.setAnimationEnabled(self.__animationEnabled)
self.inputAnchorItem.setAnimationEnabled(self.__animationEnabled)
[docs] def animationEnabled(self):
# type: () -> bool
"""
Are node animations enabled.
"""
return self.__animationEnabled
[docs] def setProcessingState(self, state):
# type: (int) -> None
"""
Set the node processing state i.e. the node is processing
(is busy) or is idle.
"""
if self.__processingState != state:
self.__processingState = state
self.shapeItem.setProcessingState(state)
if not state:
# Clear the progress meter.
self.setProgress(-1)
if self.__animationEnabled:
self.shapeItem.ping()
[docs] def processingState(self):
# type: () -> int
"""
The node processing state.
"""
return self.__processingState
processingState_ = Property(int, fget=processingState,
fset=setProcessingState)
[docs] def setProgress(self, progress):
# type: (float) -> None
"""
Set the node work progress state (number between 0 and 100).
"""
if progress is None or progress < 0 or not self.__processingState:
progress = -1.
progress = clip(progress, -1, 100.)
if self.__progress != progress:
self.__progress = progress
self.shapeItem.setProgress(progress)
self.__updateTitleText()
[docs] def progress(self):
# type: () -> float
"""
Return the node work progress state.
"""
return self.__progress
progress_ = Property(float, fget=progress, fset=setProgress,
doc="Node progress state.")
[docs] def setStatusMessage(self, message):
# type: (str) -> None
"""
Set the node status message text.
This text is displayed below the node's title.
"""
if self.__statusMessage != message:
self.__statusMessage = message
self.__updateTitleText()
def statusMessage(self):
# type: () -> str
return self.__statusMessage
[docs] def setStateMessage(self, message):
# type: (UserMessage) -> None
"""
Set a state message to display over the item.
Parameters
----------
message : UserMessage
Message to display. `message.severity` is used to determine
the icon and `message.contents` is used as a tool tip.
"""
self.__messages[message.message_id] = message
self.__updateMessages()
def setErrorMessage(self, message):
if self.__error != message:
self.__error = message
self.__updateMessages()
def setWarningMessage(self, message):
if self.__warning != message:
self.__warning = message
self.__updateMessages()
def setInfoMessage(self, message):
if self.__info != message:
self.__info = message
self.__updateMessages()
[docs] def newOutputAnchor(self, signal=None):
# type: (Optional[OutputSignal]) -> AnchorPoint
"""
Create and return a new output :class:`AnchorPoint`.
"""
if not (self.widget_description and self.widget_description.outputs):
raise ValueError("Widget has no outputs.")
anchor = AnchorPoint(self, signal=signal)
self.outputAnchorItem.addAnchor(anchor)
return anchor
[docs] def removeOutputAnchor(self, anchor):
# type: (AnchorPoint) -> None
"""
Remove output anchor.
"""
self.outputAnchorItem.removeAnchor(anchor)
[docs] def outputAnchors(self):
# type: () -> List[AnchorPoint]
"""
Return a list of all output anchor points.
"""
return self.outputAnchorItem.anchorPoints()
[docs] def setAnchorRotation(self, angle):
# type: (float) -> None
"""
Set the anchor rotation.
"""
self.inputAnchorItem.setRotation(angle)
self.outputAnchorItem.setRotation(angle)
self.anchorGeometryChanged.emit()
[docs] def anchorRotation(self):
# type: () -> float
"""
Return the anchor rotation.
"""
return self.inputAnchorItem.rotation()
[docs] def boundingRect(self):
# type: () -> QRectF
# TODO: Important because of this any time the child
# items change geometry the self.prepareGeometryChange()
# needs to be called.
if self.__boundingRect is None:
self.__boundingRect = self.childrenBoundingRect()
return QRectF(self.__boundingRect)
[docs] def shape(self):
# type: () -> QPainterPath
# Shape for mouse hit detection.
# TODO: Should this return the union of all child items?
return self.shapeItem.shape()
def __updateTitleText(self):
# type: () -> None
"""
Update the title text item.
"""
if self.captionTextItem.isEditing():
return
text = ['<div align="center">%s' % escape(self.title())]
status_text = []
progress_included = False
if self.__statusMessage:
msg = escape(self.__statusMessage)
format_fields = dict(parse_format_fields(msg))
if "progress" in format_fields and len(format_fields) == 1:
# Insert progress into the status text format string.
spec, _ = format_fields["progress"]
if spec is not None:
progress_included = True
progress_str = "{0:.0f}%".format(self.progress())
status_text.append(msg.format(progress=progress_str))
else:
status_text.append(msg)
if self.progress() >= 0 and not progress_included:
status_text.append("%i%%" % int(self.progress()))
if status_text:
text += ["<br/>",
'<span style="font-style: italic">',
"<br/>".join(status_text),
"</span>"]
text += ["</div>"]
text = "".join(text)
if self.__renderedText != text:
self.__renderedText = text
# The NodeItems boundingRect could change.
self.prepareGeometryChange()
self.__boundingRect = None
self.captionTextItem.setHtml(text)
self.__layoutCaptionTextItem()
def __layoutCaptionTextItem(self):
self.prepareGeometryChange()
self.__boundingRect = None
self.captionTextItem.document().adjustSize()
width = self.captionTextItem.textWidth()
self.captionTextItem.setPos(-width / 2.0, 33)
def __updateMessages(self):
# type: () -> None
"""
Update message items (position, visibility and tool tips).
"""
items = [self.errorItem, self.warningItem, self.infoItem]
messages = list(self.__messages.values()) + [
UserMessage(self.__error or "", UserMessage.Error,
message_id="_error"),
UserMessage(self.__warning or "", UserMessage.Warning,
message_id="_warn"),
UserMessage(self.__info or "", UserMessage.Info,
message_id="_info"),
]
key = attrgetter("severity")
messages = groupby(sorted(messages, key=key, reverse=True), key=key)
for (_, message_g), item in zip(messages, items):
message = "<br/>".join(m.contents for m in message_g if m.contents)
item.setVisible(bool(message))
if bool(message):
item.anim.start(QPropertyAnimation.KeepWhenStopped)
item.setToolTip(message or "")
shown = [item for item in items if item.isVisible()]
count = len(shown)
if count:
spacing = 3
rects = [item.boundingRect() for item in shown]
width = sum(rect.width() for rect in rects)
width += spacing * max(0, count - 1)
height = max(rect.height() for rect in rects)
origin = self.shapeItem.boundingRect().top() - spacing - height
origin = QPointF(-width / 2, origin)
for item, rect in zip(shown, rects):
item.setPos(origin)
origin = origin + QPointF(rect.width() + spacing, 0)
[docs] def mousePressEvent(self, event):
# type: (QGraphicsSceneMouseEvent) -> None
if self.mousePressTime.elapsed() < QApplication.doubleClickInterval():
# Double-click triggers two mouse press events and a double-click event.
# Ignore the second mouse press event (causes widget's node relocation with
# Logitech's Smart Move).
event.ignore()
else:
self.mousePressTime.restart()
if self.shapeItem.path().contains(event.pos()):
super().mousePressEvent(event)
else:
event.ignore()
[docs] def mouseDoubleClickEvent(self, event):
# type: (QGraphicsSceneMouseEvent) -> None
if self.shapeItem.path().contains(event.pos()):
super().mouseDoubleClickEvent(event)
QTimer.singleShot(0, self.activated.emit)
else:
event.ignore()
[docs] def changeEvent(self, event):
if event.type() == QEvent.PaletteChange:
self.__updatePalette()
elif event.type() == QEvent.FontChange:
self.__updateFont()
super().changeEvent(event)
[docs] def itemChange(self, change, value):
# type: (QGraphicsItem.GraphicsItemChange, Any) -> Any
if change == QGraphicsItem.ItemSelectedHasChanged:
self.shapeItem.setSelected(value)
self.captionTextItem.setSelectionState(value)
self.selectedChanged.emit(value)
elif change == QGraphicsItem.ItemPositionHasChanged:
self.positionChanged.emit()
return super().itemChange(change, value)
def __updatePalette(self):
# type: () -> None
palette = self.palette()
self.captionTextItem.setPalette(palette)
def __updateFont(self):
# type: () -> None
self.prepareGeometryChange()
self.captionTextItem.setFont(self.font())
self.__layoutCaptionTextItem()
TOOLTIP_TEMPLATE = """\
<html>
<head>
<style type="text/css">
{style}
</style>
</head>
<body>
{tooltip}
</body>
</html>
"""
def NodeItem_toolTipHelper(node, links_in=[], links_out=[]):
# type: (NodeItem, List[LinkItem], List[LinkItem]) -> str
"""
A helper function for constructing a standard tooltip for the node
in on the canvas.
Parameters:
===========
node : NodeItem
The node item instance.
links_in : list of LinkItem instances
A list of input links for the node.
links_out : list of LinkItem instances
A list of output links for the node.
"""
desc = node.widget_description
channel_fmt = "<li>{0}</li>"
title_fmt = "<b>{title}</b><hr/>"
title = title_fmt.format(title=escape(node.title()))
inputs_list_fmt = "Inputs:<ul>{inputs}</ul><hr/>"
outputs_list_fmt = "Outputs:<ul>{outputs}</ul>"
if desc.inputs:
inputs = [channel_fmt.format(inp.name) for inp in desc.inputs]
inputs = inputs_list_fmt.format(inputs="".join(inputs))
else:
inputs = "No inputs<hr/>"
if desc.outputs:
outputs = [channel_fmt.format(out.name) for out in desc.outputs]
outputs = outputs_list_fmt.format(outputs="".join(outputs))
else:
outputs = "No outputs"
tooltip = title + inputs + outputs
style = "ul { margin-top: 1px; margin-bottom: 1px; }"
return TOOLTIP_TEMPLATE.format(style=style, tooltip=tooltip)
def parse_format_fields(format_str):
# type: (str) -> List[Tuple[str, Tuple[Optional[str], Optional[str]]]]
formatter = string.Formatter()
format_fields = [(field, (spec, conv))
for _, field, spec, conv in formatter.parse(format_str)
if field is not None]
return format_fields
from .linkitem import LinkItem