Re-implemented Backdrop Node

This commit is contained in:
2025-02-12 23:25:02 -07:00
parent ca5152b89a
commit 9c2e287b72
19 changed files with 784 additions and 35 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

184
Nodes/backdrop_node.py Normal file
View File

@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""
Enhanced Backdrop Node (Inherited from BaseNode)
Features:
- Inherits from `BaseNode` so it can be discovered in your node scanning.
- Custom context menu to rename (set title) or pick a new color.
- Forces geometry updates to reduce "ghosting" or partial redraws.
"""
from Qt import QtWidgets, QtGui
from OdenGraphQt import BaseNode
from OdenGraphQt.constants import NodePropWidgetEnum
from OdenGraphQt.qgraphics.node_backdrop import BackdropNodeItem
class BackdropNode(BaseNode):
"""
Backdrop Node:
- Allows grouping or annotating other nodes by resizing a large rectangle.
- Provides a custom context menu for renaming and recoloring (via on_context_menu).
"""
__identifier__ = 'bunny-lab.io.backdrop'
NODE_NAME = 'Backdrop'
def __init__(self):
# Use BackdropNodeItem for the specialized QGraphicsItem.
super(BackdropNode, self).__init__(qgraphics_item=BackdropNodeItem)
# Default color (teal).
self.model.color = (5, 129, 138, 255)
# Multi-line text property for storing the backdrop text.
self.create_property(
'backdrop_text',
'',
widget_type=NodePropWidgetEnum.QTEXT_EDIT.value,
tab='Backdrop'
)
# --------------------------------------------------------------------------
# Resizing / Geometry
# --------------------------------------------------------------------------
def on_backdrop_updated(self, update_prop, value=None):
"""
Triggered when the user resizes or double-clicks the backdrop sizer handle.
"""
if not self.graph:
return
if update_prop == 'sizer_mouse_release':
# User finished dragging the resize handle
self.view.prepareGeometryChange()
self.graph.begin_undo(f'resized "{self.name()}"')
self.set_property('width', value['width'])
self.set_property('height', value['height'])
self.set_pos(*value['pos'])
self.graph.end_undo()
self.view.update()
elif update_prop == 'sizer_double_clicked':
# User double-clicked the resize handle (auto-resize)
self.view.prepareGeometryChange()
self.graph.begin_undo(f'"{self.name()}" auto resize')
self.set_property('width', value['width'])
self.set_property('height', value['height'])
self.set_pos(*value['pos'])
self.graph.end_undo()
self.view.update()
def auto_size(self):
"""
Auto-resize the backdrop to fit around intersecting nodes.
"""
if not self.graph:
return
self.view.prepareGeometryChange()
self.graph.begin_undo(f'"{self.name()}" auto resize')
size = self.view.calc_backdrop_size()
self.set_property('width', size['width'])
self.set_property('height', size['height'])
self.set_pos(*size['pos'])
self.graph.end_undo()
self.view.update()
def wrap_nodes(self, nodes):
"""
Fit the backdrop around the specified nodes.
"""
if not self.graph or not nodes:
return
self.view.prepareGeometryChange()
self.graph.begin_undo(f'"{self.name()}" wrap nodes')
size = self.view.calc_backdrop_size([n.view for n in nodes])
self.set_property('width', size['width'])
self.set_property('height', size['height'])
self.set_pos(*size['pos'])
self.graph.end_undo()
self.view.update()
def nodes(self):
"""
Return a list of nodes wrapped by this backdrop.
"""
node_ids = [n.id for n in self.view.get_nodes()]
return [self.graph.get_node_by_id(nid) for nid in node_ids]
def set_text(self, text=''):
"""
Set the multi-line text in the backdrop.
"""
self.set_property('backdrop_text', text)
def text(self):
"""
Return the text content in the backdrop.
"""
return self.get_property('backdrop_text')
def set_size(self, width, height):
"""
Manually set the backdrop size.
"""
if self.graph:
self.view.prepareGeometryChange()
self.graph.begin_undo('backdrop size')
self.set_property('width', width)
self.set_property('height', height)
self.graph.end_undo()
self.view.update()
else:
self.view.width, self.view.height = width, height
self.model.width, self.model.height = width, height
def size(self):
"""
Return (width, height) of the backdrop.
"""
self.model.width = self.view.width
self.model.height = self.view.height
return self.model.width, self.model.height
# No ports for a backdrop:
def inputs(self):
return
def outputs(self):
return
# --------------------------------------------------------------------------
# Custom Context Menu
# --------------------------------------------------------------------------
def on_context_menu(self, menu):
"""
Called manually by the node context menu callback in older NodeGraphQt versions.
"""
rename_action = menu.addAction("Set Title...")
rename_action.triggered.connect(self._change_title)
color_action = menu.addAction("Set Color...")
color_action.triggered.connect(self._change_color)
def _change_title(self):
"""
Prompt for a new backdrop title (header).
"""
new_title, ok = QtWidgets.QInputDialog.getText(
None, "Backdrop Title", "Enter new backdrop title:"
)
if ok and new_title:
self.set_name(new_title)
def _change_color(self):
"""
Prompt for a new backdrop color via QColorDialog.
"""
current_color = QtGui.QColor(*self.model.color)
color = QtWidgets.QColorDialog.getColor(
current_color, None, "Select Backdrop Color"
)
if color.isValid():
self.model.color = (color.red(), color.green(), color.blue(), color.alpha())
self.view.update()

86
Nodes/basic_nodes.py Normal file
View File

@ -0,0 +1,86 @@
from OdenGraphQt import BaseNode, BaseNodeCircle
class BasicNodeA(BaseNode):
"""
A node class with 2 inputs and 2 outputs.
"""
# unique node identifier.
__identifier__ = 'nodes.basic'
# initial default node name.
NODE_NAME = 'node A'
def __init__(self):
super(BasicNodeA, self).__init__()
# create node inputs.
self.add_input('in A')
self.add_input('in B')
# create node outputs.
self.add_output('out A')
self.add_output('out B')
class BasicNodeB(BaseNode):
"""
A node class with 3 inputs and 3 outputs.
The last input and last output can take in multiple pipes.
"""
# unique node identifier.
__identifier__ = 'nodes.basic'
# initial default node name.
NODE_NAME = 'node B'
def __init__(self):
super(BasicNodeB, self).__init__()
# create node inputs
self.add_input('single 1')
self.add_input('single 2')
self.add_input('multi in', multi_input=True)
# create node outputs
self.add_output('single 1', multi_output=False)
self.add_output('single 2', multi_output=False)
self.add_output('multi out')
class CircleNode(BaseNodeCircle):
"""
A node class with 3 inputs and 3 outputs.
This node is a circular design.
"""
# unique node identifier.
__identifier__ = 'nodes.basic'
# initial default node name.
NODE_NAME = 'Circle Node'
def __init__(self):
super(CircleNode, self).__init__()
self.set_color(10, 24, 38)
# create node inputs
p = self.add_input('in 1')
p.add_accept_port_type(
port_name='single 1',
port_type='out',
node_type='nodes.basic.BasicNodeB'
)
self.add_input('in 2')
self.add_input('in 3', multi_input=True)
self.add_input('in 4', display_name=False)
self.add_input('in 5', display_name=False)
# create node outputs
self.add_output('out 1')
self.add_output('out 2', multi_output=False)
self.add_output('out 3', multi_output=True, display_name=False)
self.add_output('out 4', multi_output=True, display_name=False)

121
Nodes/custom_ports_node.py Normal file
View File

@ -0,0 +1,121 @@
#!/usr/bin/python
from qtpy import QtCore, QtGui
from OdenGraphQt import BaseNode
def draw_triangle_port(painter, rect, info):
"""
Custom paint function for drawing a Triangle shaped port.
Args:
painter (QtGui.QPainter): painter object.
rect (QtCore.QRectF): port rect used to describe parameters
needed to draw.
info (dict): information describing the ports current state.
{
'port_type': 'in',
'color': (0, 0, 0),
'border_color': (255, 255, 255),
'multi_connection': False,
'connected': False,
'hovered': False,
}
"""
painter.save()
size = int(rect.height() / 2)
triangle = QtGui.QPolygonF()
triangle.append(QtCore.QPointF(-size, size))
triangle.append(QtCore.QPointF(0.0, -size))
triangle.append(QtCore.QPointF(size, size))
transform = QtGui.QTransform()
transform.translate(rect.center().x(), rect.center().y())
port_poly = transform.map(triangle)
# mouse over port color.
if info['hovered']:
color = QtGui.QColor(14, 45, 59)
border_color = QtGui.QColor(136, 255, 35)
# port connected color.
elif info['connected']:
color = QtGui.QColor(195, 60, 60)
border_color = QtGui.QColor(200, 130, 70)
# default port color
else:
color = QtGui.QColor(*info['color'])
border_color = QtGui.QColor(*info['border_color'])
pen = QtGui.QPen(border_color, 1.8)
pen.setJoinStyle(QtCore.Qt.PenJoinStyle.MiterJoin)
painter.setPen(pen)
painter.setBrush(color)
painter.drawPolygon(port_poly)
painter.restore()
def draw_square_port(painter, rect, info):
"""
Custom paint function for drawing a Square shaped port.
Args:
painter (QtGui.QPainter): painter object.
rect (QtCore.QRectF): port rect used to describe parameters
needed to draw.
info (dict): information describing the ports current state.
{
'port_type': 'in',
'color': (0, 0, 0),
'border_color': (255, 255, 255),
'multi_connection': False,
'connected': False,
'hovered': False,
}
"""
painter.save()
# mouse over port color.
if info['hovered']:
color = QtGui.QColor(14, 45, 59)
border_color = QtGui.QColor(136, 255, 35, 255)
# port connected color.
elif info['connected']:
color = QtGui.QColor(195, 60, 60)
border_color = QtGui.QColor(200, 130, 70)
# default port color
else:
color = QtGui.QColor(*info['color'])
border_color = QtGui.QColor(*info['border_color'])
pen = QtGui.QPen(border_color, 1.8)
pen.setJoinStyle(QtCore.Qt.PenJoinStyle.MiterJoin)
painter.setPen(pen)
painter.setBrush(color)
painter.drawRect(rect)
painter.restore()
class CustomPortsNode(BaseNode):
"""
example test node with custom shaped ports.
"""
# set a unique node identifier.
__identifier__ = 'nodes.custom.ports'
# set the initial default node name.
NODE_NAME = 'node'
def __init__(self):
super(CustomPortsNode, self).__init__()
# create input and output port.
self.add_input('in', color=(200, 10, 0))
self.add_output('default')
self.add_output('square', painter_func=draw_square_port)
self.add_output('triangle', painter_func=draw_triangle_port)

View File

@ -1,13 +1,4 @@
#!/usr/bin/env python3
"""
Character Status Node
This node represents the character's status. It has seven output ports:
- HP: Current, HP: Total, MP: Current, MP: Total, FP: Current, FP: Total, EXP.
It polls an API endpoint (http://127.0.0.1:5000/data) every 500 ms to update its values.
If the API call is successful, the node's title is set to "Character Status (API Connected)".
If the API is down or returns an error, the title is set to "Character Status (API Disconnected)".
"""
from OdenGraphQt import BaseNode
from Qt import QtCore, QtGui
@ -44,7 +35,7 @@ def get_draw_stat_port(color, border_color=None, alpha=127):
return painter_func
class CharacterStatusNode(BaseNode):
__identifier__ = 'bunny-lab.io.status_node'
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
NODE_NAME = 'Character Status'
def __init__(self):

21
Nodes/group_node.py Normal file
View File

@ -0,0 +1,21 @@
from OdenGraphQt import GroupNode
class MyGroupNode(GroupNode):
"""
example test group node with a in port and out port.
"""
# set a unique node identifier.
__identifier__ = 'nodes.group'
# set the initial default node name.
NODE_NAME = 'group node'
def __init__(self):
super(MyGroupNode, self).__init__()
self.set_color(50, 8, 25)
# create input and output port.
self.add_input('in')
self.add_output('out')

155
Nodes/widget_node.py Normal file
View File

@ -0,0 +1,155 @@
from OdenGraphQt import BaseNode
from OdenGraphQt.constants import NodePropWidgetEnum
from OdenGraphQt.widgets.node_widgets import NodeLineEditValidatorCheckBox
class DropdownMenuNode(BaseNode):
"""
An example node with a embedded added QCombobox menu.
"""
# unique node identifier.
__identifier__ = 'nodes.widget'
# initial default node name.
NODE_NAME = 'menu'
def __init__(self):
super(DropdownMenuNode, self).__init__()
# create input & output ports
self.add_input('in 1')
self.add_output('out 1')
self.add_output('out 2')
# create the QComboBox menu.
items = ["item 1", "item 2", "item 3"]
self.add_combo_menu(
"my_menu",
"Menu Test",
items=items,
tooltip="example custom tooltip",
)
class TextInputNode(BaseNode):
"""
An example of a node with a embedded QLineEdit.
"""
# unique node identifier.
__identifier__ = 'nodes.widget'
# initial default node name.
NODE_NAME = 'text'
def __init__(self):
super().__init__()
pattern = r"^[A-Za-z0-9]*$"
placeholder = ""
tooltip = "Valid characters: A-Z a-z 0-9"
is_case_sensitive = True
checkbox_label = "Use Parser?"
# create input & output ports
self.add_input('in')
self.add_output('out')
# create QLineEdit text input widget.
self.add_text_input('my_input', 'Text Input', tab='widgets')
tool_btn_kwargs = {
"func": self._callback,
"tooltip": "Awesome"
}
kwargs = {
"validator": {
"pattern": pattern,
"placeholder": placeholder,
"tooltip": tooltip,
"is_case_insensitive": is_case_sensitive,
"checkbox_visible": True,
"tool_btn_visible": True,
},
"checkbox_label": checkbox_label,
"tool_btn": tool_btn_kwargs,
}
node_widget = NodeLineEditValidatorCheckBox(
"src_path",
pattern,
placeholder,
tooltip,
is_case_sensitive,
checkbox_label,
checkbox_visible=True,
tool_btn_visible=True,
widget_label="src_path",
parent=self.view,
)
node_widget.get_custom_widget().set_tool_btn(**tool_btn_kwargs)
self.add_custom_widget(
node_widget,
NodePropWidgetEnum.LINEEDIT_VALIDATOR_CHECKBOX.value,
"widgets",
**kwargs,
)
kwargs2 = {
"validator": {
"pattern": pattern,
"placeholder": placeholder,
"tooltip": tooltip,
"is_case_insensitive": is_case_sensitive,
"checkbox_visible": False,
"tool_btn_visible": False,
},
"checkbox_label": "Check In Luggage?",
"tool_btn": tool_btn_kwargs,
}
node_widget2 = NodeLineEditValidatorCheckBox(
"dst_path",
pattern,
placeholder,
tooltip,
is_case_sensitive,
"Check In Luggage?",
checkbox_visible=False,
tool_btn_visible=False,
widget_label="dst_path",
parent=self.view,
)
node_widget2.get_custom_widget().set_tool_btn(**tool_btn_kwargs)
node_widget2.set_checkbox_visible(False)
node_widget2.set_tool_btn_visible(False)
self.add_custom_widget(
node_widget2,
NodePropWidgetEnum.LINEEDIT_VALIDATOR_CHECKBOX.value,
"widgets",
**kwargs2,
)
def _callback(self):
print(f"YOU HAVE CLICKED ON '{self.NODE_NAME}'")
class CheckboxNode(BaseNode):
"""
An example of a node with 2 embedded QCheckBox widgets.
"""
# set a unique node identifier.
__identifier__ = 'nodes.widget'
# set the initial default node name.
NODE_NAME = 'checkbox'
def __init__(self):
super(CheckboxNode, self).__init__()
# create the checkboxes.
self.add_checkbox('cb_1', '', 'Checkbox 1', True)
self.add_checkbox('cb_2', '', 'Checkbox 2', False)
# create input and output port.
self.add_input('in', color=(200, 100, 0))
self.add_output('out', color=(0, 100, 200))