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
import sys
import os
import inspect
import pkgutil
import importlib
import inspect
import os
from Qt import QtWidgets, QtCore, QtGui
# ------------------------------------------------------------------
# MONKEY-PATCH to override all pens in PipeItem.__init__ so
# idle, hover, selected, etc. all share the same color.
# -------------------------------------------------------#
# MONKEY PATCHES - MODIFICATIONS TO OdenGraphQT BEHAVIOR #
# -------------------------------------------------------#
# PATCH: Override the color of interconnection pipes between nodes
try:
from OdenGraphQt.qgraphics.pipe import PipeItem
from qtpy.QtGui import QPen, QColor
@ -55,33 +58,24 @@ except ImportError:
print("WARNING: Could not patch PipeItem paint method.")
except Exception as e:
print(f"Patch for PipeItem.paint override failed: {e}")
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# MONKEY-PATCH to fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
# PATCH: Fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
try:
from qtpy.QtWidgets import QUndoStack
import qtpy
qtpy.QtGui.QUndoStack = QUndoStack
except ImportError:
print("WARNING: Could not monkey-patch QUndoStack. You may see an error if OdenGraphQt needs it.")
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# MONKEY-PATCH to fix "'BackdropNodeItem' object has no attribute 'widgets'"
# PATCH: Fix "'BackdropNodeItem' object has no attribute 'widgets'" by giving BackdropNodeItem a trivial widgets dictionary.
try:
from OdenGraphQt.nodes.backdrop_node import BackdropNodeItem
if not hasattr(BackdropNodeItem, "widgets"):
BackdropNodeItem.widgets = {}
except ImportError:
print("WARNING: Could not monkey-patch BackdropNodeItem to add `widgets`.")
# ------------------------------------------------------------------
# Import your data_manager so we can start the Flask server
from Modules import data_manager
data_manager.start_api_server()
# --- BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea ---
# PATCH: BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea
_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea
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)
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.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):
"""
Recursively import all modules from the given package.
Returns a dictionary where keys are subfolder names,
and values are lists of BaseNode subclasses.
Returns a dictionary where keys are subfolder names, and values are lists of BaseNode subclasses.
"""
nodes_by_category = {}
package = importlib.import_module(package_name)
@ -193,14 +127,13 @@ def import_nodes_from_folder(package_name):
nodes_by_category[category_name].append(obj)
except Exception as e:
print(f"Failed to import {module_name}: {e}")
return nodes_by_category
def make_node_command(graph, node_type_str):
"""
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():
if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"):
@ -226,175 +159,129 @@ def make_node_command(graph, node_type_str):
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 __init__(self):
super().__init__()
def close_workflow(graph: NodeGraph):
"""
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
self.graph = NodeGraph()
if not file_path.lower().endswith(".json"):
file_path += ".json"
# Grid styling changes
self.graph.set_background_color(20, 20, 20) # Dark gray
self.graph.set_grid_color(60, 60, 60) # Gray grid lines
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))
# Add gradient background
scene = self.graph.scene()
gradient = QtGui.QLinearGradient(0, 0, 0, 1)
gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68))
gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30))
gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30))
gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68))
scene.setBackgroundBrush(QtGui.QBrush(gradient))
# Load custom nodes from "Nodes" folder
custom_nodes_by_category = import_nodes_from_folder("Nodes")
for category, node_classes in custom_nodes_by_category.items():
for node_class in node_classes:
self.graph.register_node(node_class)
# Build the right-click context menu for the graph
self._build_graph_context_menu(custom_nodes_by_category)
# Setup the central widget to show the NodeGraph
self.setCentralWidget(self.graph.widget)
# 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_())
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__":
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
graph.set_background_color(20, 20, 20) # Dark gray
graph.set_grid_color(60, 60, 60) # Gray grid lines
# Add gradient background
scene = graph.scene()
gradient = QtGui.QLinearGradient(0, 0, 0, 1)
gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68))
gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30))
gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30))
gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68))
scene.setBackgroundBrush(QtGui.QBrush(gradient))
# Resize and show the graph widget
graph.widget.resize(1600, 900)
graph.widget.show()
# Global update function
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)
timer = QtCore.QTimer()
timer.timeout.connect(global_update)
timer.start(500)
sys.exit(app.exec_())