diff --git a/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc index cbe246a..25f26b3 100644 Binary files a/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc and b/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc differ diff --git a/Workflows/Basic_Data_Node_Connection.json b/Workflows/Basic_Data_Node_Connection.json new file mode 100644 index 0000000..902306b --- /dev/null +++ b/Workflows/Basic_Data_Node_Connection.json @@ -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" + ] + } + ] +} \ No newline at end of file diff --git a/__pycache__/monkey_patches.cpython-312.pyc b/__pycache__/monkey_patches.cpython-312.pyc new file mode 100644 index 0000000..2b29442 Binary files /dev/null and b/__pycache__/monkey_patches.cpython-312.pyc differ diff --git a/borealis.py b/borealis.py index aa28109..274c9d9 100644 --- a/borealis.py +++ b/borealis.py @@ -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( - "" - "Flask API Server: http://127.0.0.1:5000/data  " - ) - 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_())