Reincorporated Old Code with Pipe Color Overrides

This commit is contained in:
Nicole Rappe 2025-02-25 19:24:38 -07:00
parent 78ca945581
commit 4591178a4b
4 changed files with 236 additions and 248 deletions

View File

@ -0,0 +1,101 @@
{
"graph":{
"layout_direction":0,
"acyclic":true,
"pipe_collision":false,
"pipe_slicing":true,
"pipe_style":1,
"accept_connection_types":{},
"reject_connection_types":{}
},
"nodes":{
"0x1ad82a5c620":{
"type_":"bunny-lab.io.data_node.DataNode",
"icon":null,
"name":"Data Node",
"color":[
13,
18,
23,
255
],
"border_color":[
74,
84,
85,
255
],
"text_color":[
255,
255,
255,
180
],
"disabled":false,
"selected":false,
"visible":true,
"width":269.0,
"height":74.2,
"pos":[
-93.6890385514249,
181.13214119942148
],
"layout_direction":0,
"port_deletion_allowed":false,
"subgraph_session":{},
"custom":{
"value":"57"
}
},
"0x1ad82a5cef0":{
"type_":"bunny-lab.io.data_node.DataNode",
"icon":null,
"name":"Data Node 1",
"color":[
13,
18,
23,
255
],
"border_color":[
74,
84,
85,
255
],
"text_color":[
255,
255,
255,
180
],
"disabled":false,
"selected":false,
"visible":true,
"width":269.0,
"height":74.2,
"pos":[
361.37200584121035,
287.313051557703
],
"layout_direction":0,
"port_deletion_allowed":false,
"subgraph_session":{},
"custom":{
"value":"57"
}
}
},
"connections":[
{
"out":[
"0x1ad82a5c620",
"Output"
],
"in":[
"0x1ad82a5cef0",
"Input"
]
}
]
}

Binary file not shown.

View File

@ -2,15 +2,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
import os import pkgutil
import inspect
import importlib import importlib
import inspect
import os
from Qt import QtWidgets, QtCore, QtGui from Qt import QtWidgets, QtCore, QtGui
# ------------------------------------------------------------------ # -------------------------------------------------------#
# MONKEY-PATCH to override all pens in PipeItem.__init__ so # MONKEY PATCHES - MODIFICATIONS TO OdenGraphQT BEHAVIOR #
# idle, hover, selected, etc. all share the same color. # -------------------------------------------------------#
# PATCH: Override the color of interconnection pipes between nodes
try: try:
from OdenGraphQt.qgraphics.pipe import PipeItem from OdenGraphQt.qgraphics.pipe import PipeItem
from qtpy.QtGui import QPen, QColor from qtpy.QtGui import QPen, QColor
@ -55,33 +58,24 @@ except ImportError:
print("WARNING: Could not patch PipeItem paint method.") print("WARNING: Could not patch PipeItem paint method.")
except Exception as e: except Exception as e:
print(f"Patch for PipeItem.paint override failed: {e}") print(f"Patch for PipeItem.paint override failed: {e}")
# ------------------------------------------------------------------
# ------------------------------------------------------------------ # PATCH: Fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
# MONKEY-PATCH to fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
try: try:
from qtpy.QtWidgets import QUndoStack from qtpy.QtWidgets import QUndoStack
import qtpy import qtpy
qtpy.QtGui.QUndoStack = QUndoStack qtpy.QtGui.QUndoStack = QUndoStack
except ImportError: except ImportError:
print("WARNING: Could not monkey-patch QUndoStack. You may see an error if OdenGraphQt needs it.") print("WARNING: Could not monkey-patch QUndoStack. You may see an error if OdenGraphQt needs it.")
# ------------------------------------------------------------------
# ------------------------------------------------------------------ # PATCH: Fix "'BackdropNodeItem' object has no attribute 'widgets'" by giving BackdropNodeItem a trivial widgets dictionary.
# MONKEY-PATCH to fix "'BackdropNodeItem' object has no attribute 'widgets'"
try: try:
from OdenGraphQt.nodes.backdrop_node import BackdropNodeItem from OdenGraphQt.nodes.backdrop_node import BackdropNodeItem
if not hasattr(BackdropNodeItem, "widgets"): if not hasattr(BackdropNodeItem, "widgets"):
BackdropNodeItem.widgets = {} BackdropNodeItem.widgets = {}
except ImportError: except ImportError:
print("WARNING: Could not monkey-patch BackdropNodeItem to add `widgets`.") print("WARNING: Could not monkey-patch BackdropNodeItem to add `widgets`.")
# ------------------------------------------------------------------
# Import your data_manager so we can start the Flask server # PATCH: BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea
from Modules import data_manager
data_manager.start_api_server()
# --- BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea ---
_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea _original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea
def _patched_setSelectionArea(self, *args, **kwargs): def _patched_setSelectionArea(self, *args, **kwargs):
@ -100,77 +94,17 @@ def _patched_setSelectionArea(self, *args, **kwargs):
return _original_setSelectionArea(self, painterPath, selection_op, selection_mode, transform) return _original_setSelectionArea(self, painterPath, selection_op, selection_mode, transform)
QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea
# --- END PATCH ---
# Import your data_manager so we can start the Flask server
from Modules import data_manager
from OdenGraphQt import NodeGraph, BaseNode from OdenGraphQt import NodeGraph, BaseNode
from OdenGraphQt.widgets.dialogs import FileDialog from OdenGraphQt.widgets.dialogs import FileDialog
def ensure_workflows_folder():
"""
Ensures a 'Workflows' subfolder exists.
"""
if not os.path.exists("Workflows"):
os.makedirs("Workflows")
def close_workflow(graph: NodeGraph):
"""
Closes the current workflow (removes all nodes and connections).
"""
graph.clear_session()
def save_workflow(graph: NodeGraph):
"""
Saves the current workflow (including custom names, positions, wires, etc.)
into a JSON file in the 'Workflows' subfolder.
"""
ensure_workflows_folder()
file_filter = "JSON Files (*.json);;All Files (*.*)"
dlg = FileDialog.getSaveFileName(None, "Save Workflow", os.path.join("Workflows", ""), file_filter)
file_path = dlg[0]
if not file_path:
return # User canceled
if not file_path.lower().endswith(".json"):
file_path += ".json"
try:
graph.save_session(file_path)
print(f"Workflow saved to {file_path}")
except Exception as e:
QtWidgets.QMessageBox.critical(None, "Error Saving Workflow", str(e))
def load_workflow(graph: NodeGraph):
"""
Loads a workflow (including node values, connections, positions, etc.)
from a specified JSON file, then centers the view on all loaded nodes.
"""
ensure_workflows_folder()
file_filter = "JSON Files (*.json);;All Files (*.*)"
dlg = FileDialog.getOpenFileName(None, "Load Workflow", os.path.join("Workflows", ""), file_filter)
file_path = dlg[0]
if not file_path:
return # User canceled
try:
graph.load_session(file_path)
# After loading, center the viewer on all nodes.
all_nodes = graph.all_nodes()
if all_nodes:
graph.viewer().zoom_to_nodes([node.view for node in all_nodes])
print(f"Workflow loaded from {file_path}")
except Exception as e:
QtWidgets.QMessageBox.critical(None, "Error Loading Workflow", str(e))
def import_nodes_from_folder(package_name): def import_nodes_from_folder(package_name):
""" """
Recursively import all modules from the given package. Recursively import all modules from the given package.
Returns a dictionary where keys are subfolder names, Returns a dictionary where keys are subfolder names, and values are lists of BaseNode subclasses.
and values are lists of BaseNode subclasses.
""" """
nodes_by_category = {} nodes_by_category = {}
package = importlib.import_module(package_name) package = importlib.import_module(package_name)
@ -196,11 +130,10 @@ def import_nodes_from_folder(package_name):
return nodes_by_category return nodes_by_category
def make_node_command(graph, node_type_str): def make_node_command(graph, node_type_str):
""" """
Return a function that creates a node of the given type at the current cursor position. Return a function that creates a node of the given type at the current cursor position.
Ensures that only one FlyffCharacterStatusNode exists if that's the node being created. Ensures that only one FlyffCharacterStatusNode exists.
""" """
def real_create(): def real_create():
if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"): if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"):
@ -226,22 +159,106 @@ def make_node_command(graph, node_type_str):
return command return command
def ensure_workflows_folder():
"""
Ensures a 'Workflows' subfolder exists.
"""
if not os.path.exists("Workflows"):
os.makedirs("Workflows")
class BorealisWindow(QtWidgets.QMainWindow): def close_workflow(graph: NodeGraph):
def __init__(self): """
super().__init__() Closes the current workflow (removes all nodes and connections).
"""
graph.clear_session()
self.setWindowTitle("Borealis - Workflow Automation System") def save_workflow(graph: NodeGraph):
"""
Saves the current workflow (including custom names, positions, wires, etc.) into a JSON file
in the 'Workflows' subfolder.
"""
ensure_workflows_folder()
file_filter = "JSON Files (*.json);;All Files (*.*)"
dlg = FileDialog.getSaveFileName(None, "Save Workflow", os.path.join("Workflows", ""), file_filter)
file_path = dlg[0]
if not file_path:
return # User canceled
# Create NodeGraph and widget if not file_path.lower().endswith(".json"):
self.graph = NodeGraph() file_path += ".json"
try:
graph.save_session(file_path)
print(f"Workflow saved to {file_path}")
except Exception as e:
QtWidgets.QMessageBox.critical(None, "Error Saving Workflow", str(e))
def load_workflow(graph: NodeGraph):
"""
Loads a workflow (including node values, connections, positions, etc.) from a specified JSON file.
"""
ensure_workflows_folder()
file_filter = "JSON Files (*.json);;All Files (*.*)"
dlg = FileDialog.getOpenFileName(None, "Load Workflow", os.path.join("Workflows", ""), file_filter)
file_path = dlg[0]
if not file_path:
return # User canceled
try:
graph.load_session(file_path)
print(f"Workflow loaded from {file_path}")
except Exception as e:
QtWidgets.QMessageBox.critical(None, "Error Loading Workflow", str(e))
if __name__ == "__main__":
app = QtWidgets.QApplication([])
# Start Flask API Server
data_manager.start_api_server()
# Create the NodeGraph
graph = NodeGraph()
graph.widget.setWindowTitle("Project Borealis - Workflow Automation System")
# Dynamically import custom node classes from the 'Nodes' package.
custom_nodes_by_category = import_nodes_from_folder("Nodes")
# Register each node in its category
for category, node_classes in custom_nodes_by_category.items():
for node_class in node_classes:
graph.register_node(node_class)
# Create categorized context menu
graph_context_menu = graph.get_context_menu("graph")
for category, node_classes in custom_nodes_by_category.items():
category_menu = graph_context_menu.add_menu(category) # Create submenu for category
for node_class in node_classes:
node_type = f"{node_class.__identifier__}.{node_class.__name__}"
node_name = node_class.NODE_NAME
category_menu.add_command(
f"Create: {node_name}",
make_node_command(graph, node_type)
)
# Add a "Remove Selected Node" command
graph_context_menu.add_command(
"Remove Selected Node",
lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None
)
# Add workflow menu commands
workflow_menu = graph_context_menu.add_menu("Workflow")
workflow_menu.add_command("Load Workflow", lambda: load_workflow(graph))
workflow_menu.add_command("Save Workflow", lambda: save_workflow(graph))
workflow_menu.add_command("Close Workflow", lambda: close_workflow(graph))
# Grid styling changes # Grid styling changes
self.graph.set_background_color(20, 20, 20) # Dark gray graph.set_background_color(20, 20, 20) # Dark gray
self.graph.set_grid_color(60, 60, 60) # Gray grid lines graph.set_grid_color(60, 60, 60) # Gray grid lines
# Add gradient background # Add gradient background
scene = self.graph.scene() scene = graph.scene()
gradient = QtGui.QLinearGradient(0, 0, 0, 1) gradient = QtGui.QLinearGradient(0, 0, 0, 1)
gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode) gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68)) gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68))
@ -250,151 +267,21 @@ class BorealisWindow(QtWidgets.QMainWindow):
gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68)) gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68))
scene.setBackgroundBrush(QtGui.QBrush(gradient)) scene.setBackgroundBrush(QtGui.QBrush(gradient))
# Load custom nodes from "Nodes" folder # Resize and show the graph widget
custom_nodes_by_category = import_nodes_from_folder("Nodes") graph.widget.resize(1600, 900)
for category, node_classes in custom_nodes_by_category.items(): graph.widget.show()
for node_class in node_classes:
self.graph.register_node(node_class)
# Build the right-click context menu for the graph # Global update function
self._build_graph_context_menu(custom_nodes_by_category) def global_update():
for node in graph.all_nodes():
if hasattr(node, "process_input"):
try:
node.process_input()
except Exception as e:
print("Error updating node", node, e)
# Setup the central widget to show the NodeGraph timer = QtCore.QTimer()
self.setCentralWidget(self.graph.widget) timer.timeout.connect(global_update)
timer.start(500)
# Build the top menu bar
self._build_menubar()
# Create a status bar
self.setStatusBar(QtWidgets.QStatusBar(self))
# Remove the resize handle from the status bar:
self.statusBar().setSizeGripEnabled(False)
# Add a permanent clickable link to the right side of the status bar:
self.link_label = QtWidgets.QLabel()
self.link_label.setTextFormat(QtCore.Qt.RichText)
self.link_label.setOpenExternalLinks(True)
# Add a couple spaces after the URL using  
# Also color style to match your "blue-ish" highlight:
self.link_label.setText(
"<a href='http://127.0.0.1:5000/data' "
"style='color: rgb(60,120,180); text-decoration: none;'>"
"Flask API Server: http://127.0.0.1:5000/data&nbsp;&nbsp;</a>"
)
self.statusBar().addPermanentWidget(self.link_label)
# Resize
self.resize(1200, 800)
def _build_graph_context_menu(self, custom_nodes_by_category):
"""
Build context menu and apply custom stylesheet for the 'blue-ish' highlight.
"""
graph_context_menu = self.graph.get_context_menu("graph")
menu_stylesheet = """
QMenu {
background-color: rgb(30, 30, 30);
border: 1px solid rgba(200, 200, 200, 60);
}
QMenu::item {
padding: 5px 18px 2px;
background-color: transparent;
}
QMenu::item:selected {
color: rgb(255, 255, 255);
background-color: rgba(60, 120, 180, 150);
}
QMenu::separator {
height: 1px;
background: rgba(255, 255, 255, 50);
margin: 4px 8px;
}
"""
if graph_context_menu and graph_context_menu.qmenu:
graph_context_menu.qmenu.setStyleSheet(menu_stylesheet)
add_nodes_menu = graph_context_menu.add_menu("Add Nodes")
if add_nodes_menu and add_nodes_menu.qmenu:
add_nodes_menu.qmenu.setStyleSheet(menu_stylesheet)
for category, node_classes in custom_nodes_by_category.items():
category_menu = add_nodes_menu.add_menu(category)
if category_menu and category_menu.qmenu:
category_menu.qmenu.setStyleSheet(menu_stylesheet)
for node_class in node_classes:
node_type = f"{node_class.__identifier__}.{node_class.__name__}"
node_name = node_class.NODE_NAME
category_menu.add_command(
node_name,
make_node_command(self.graph, node_type)
)
graph_context_menu.add_command(
"Remove Selected Node",
lambda: [self.graph.remove_node(node)
for node in self.graph.selected_nodes()]
if self.graph.selected_nodes() else None
)
def _build_menubar(self):
menubar = self.menuBar()
# Workflows menu
workflows_menu = menubar.addMenu("Workflows")
load_action = QtWidgets.QAction("Load Workflow", self)
load_action.triggered.connect(lambda: load_workflow(self.graph))
workflows_menu.addAction(load_action)
save_action = QtWidgets.QAction("Save Workflow", self)
save_action.triggered.connect(lambda: save_workflow(self.graph))
workflows_menu.addAction(save_action)
close_action = QtWidgets.QAction("Close Workflow", self)
close_action.triggered.connect(lambda: close_workflow(self.graph))
workflows_menu.addAction(close_action)
# About menu
about_menu = menubar.addMenu("About")
gitea_action = QtWidgets.QAction("Gitea Project", self)
gitea_action.triggered.connect(self._open_gitea_project)
about_menu.addAction(gitea_action)
credits_action = QtWidgets.QAction("Credits", self)
credits_action.triggered.connect(self._show_credits_popup)
about_menu.addAction(credits_action)
updates_action = QtWidgets.QAction("Check for Updates", self)
updates_action.triggered.connect(self._show_updates_popup)
about_menu.addAction(updates_action)
def _open_gitea_project(self):
url = QtCore.QUrl("https://git.bunny-lab.io/Scripts/Project_Borealis")
QtGui.QDesktopServices.openUrl(url)
def _show_credits_popup(self):
QtWidgets.QMessageBox.information(
self,
"Credits",
"Created by Nicole Rappe"
)
def _show_updates_popup(self):
QtWidgets.QMessageBox.information(
self,
"Check for Updates",
"Built-in update functionality has not been built yet, but it's on the roadmap. Stay tuned."
)
def main():
app = QtWidgets.QApplication(sys.argv)
window = BorealisWindow()
window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())
if __name__ == "__main__":
main()