# -*- coding: utf-8 -*- #!/usr/bin/env python3 import sys import pkgutil import importlib import inspect import os from Qt import QtWidgets, QtCore, QtGui # ------------------------------------------------------------------ # MONKEY-PATCH to fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'" # OdenGraphQt tries to do QtGui.QUndoStack(self). # We'll import QUndoStack from QtWidgets and attach it to QtGui. try: from qtpy.QtWidgets import QUndoStack import qtpy # Force QtGui.QUndoStack to reference QtWidgets.QUndoStack qtpy.QtGui.QUndoStack = QUndoStack except ImportError: print("WARNING: Could not monkey-patch QUndoStack. You may see an error if OdenGraphQt needs it.") # ------------------------------------------------------------------ # Import your data_manager so we can start the Flask server from Modules import data_manager # --- BEGIN MONKEY PATCH FOR PIPE COLOR (Optional) --- # If you want custom pipe colors, uncomment this patch: """ try: from OdenGraphQt.qgraphics.pipe import PipeItem _orig_pipeitem_init = PipeItem.__init__ def _new_pipeitem_init(self, *args, **kwargs): _orig_pipeitem_init(self, *args, **kwargs) new_color = QtGui.QColor(29, 202, 151) self._pen = QtGui.QPen(new_color, 2.0) self._pen_dragging = QtGui.QPen(new_color, 2.0) PipeItem.__init__ = _new_pipeitem_init except ImportError: print("WARNING: Could not patch PipeItem color - OdenGraphQt.qgraphics.pipe not found.") """ # --- END MONKEY PATCH FOR PIPE COLOR --- # --- BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea --- _original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea def _patched_setSelectionArea(self, *args, **kwargs): """ A robust patch that handles various call signatures for QGraphicsScene.setSelectionArea(). We try calling the original method with whatever arguments are provided. If a TypeError occurs, we assume it was missing some arguments and re-call with defaults. """ try: # First, try the original call with the given arguments. return _original_setSelectionArea(self, *args, **kwargs) except TypeError: # If a TypeError occurs, the caller likely used a minimal signature. # We'll fallback to a known signature with default arguments. if not args: raise # If no args at all, we cannot fix it. painterPath = args[0] # QPainterPath selection_op = QtCore.Qt.ReplaceSelection selection_mode = QtCore.Qt.IntersectsItemShape transform = QtGui.QTransform() return _original_setSelectionArea(self, painterPath, selection_op, selection_mode, transform) QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea # --- END ROBUST PATCH FOR QGraphicsScene.setSelectionArea --- from OdenGraphQt import NodeGraph, BaseNode def import_nodes_from_folder(package_name): """ Recursively import all modules from the given package and return a list of classes that subclass BaseNode. """ imported_nodes = [] package = importlib.import_module(package_name) # Get the root directory of the package package_path = package.__path__[0] for root, _, files in os.walk(package_path): rel_path = os.path.relpath(root, package_path).replace(os.sep, '.') module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name for file in files: if file.endswith(".py") and file != "__init__.py": module_name = f"{module_prefix}.{file[:-3]}" try: module = importlib.import_module(module_name) for name, obj in inspect.getmembers(module, inspect.isclass): if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: imported_nodes.append(obj) except Exception as e: print(f"Failed to import {module_name}: {e}") return imported_nodes def make_node_command(graph, node_type_str): """ Return a function that creates a node of the given type at the current cursor position. For the Flyff Character Status Collector node, check if one already exists. If so, schedule an error message to be shown. Also ensure that node creation is delayed until after QApplication is up, to avoid 'QWidget: Must construct a QApplication before a QWidget' errors. """ def real_create(): # Check if we are about to create a duplicate Character Status Collector node. if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"): for node in graph.all_nodes(): if node.__class__.__name__ == "FlyffCharacterStatusNode": # Show error message about duplicates QtWidgets.QMessageBox.critical( None, "Error", "Only one Flyff Character Status Collector node is allowed. If you added more, things would break (really) badly." ) return try: pos = graph.cursor_pos() graph.create_node(node_type_str, pos=pos) except Exception as e: QtWidgets.QMessageBox.critical(None, "Error", str(e)) def command(): # If there's already a QApplication running, just create the node now. if QtWidgets.QApplication.instance(): real_create() else: # Otherwise, schedule the node creation for the next event cycle. QtCore.QTimer.singleShot(0, real_create) return command if __name__ == "__main__": # Create the QApplication first app = QtWidgets.QApplication([]) # Start the Flask server from data_manager so /data is always available data_manager.start_api_server() # Create the NodeGraph controller # (the monkey-patch ensures NodeGraph won't crash if it tries QtGui.QUndoStack(self)) graph = NodeGraph() graph.widget.setWindowTitle("Project Borealis - Workflow Automation System") # Dynamically import custom node classes from the 'Nodes' package. custom_nodes = import_nodes_from_folder("Nodes") for node_class in custom_nodes: graph.register_node(node_class) # Add context menu commands for dynamic node creation. graph_context_menu = graph.get_context_menu("graph") for node_class in custom_nodes: node_type = f"{node_class.__identifier__}.{node_class.__name__}" node_name = node_class.NODE_NAME graph_context_menu.add_command( f"Add {node_name}", make_node_command(graph, node_type) ) # Add a "Remove Selected Node" command to the graph context menu. 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 ) # Grid styling changes # 1) Dark background color graph.set_background_color(20, 20, 20) # Dark gray # 2) Subdued grid color graph.set_grid_color(60, 60, 60) # Gray grid lines # Optionally, create a subtle gradient in the scene: scene = graph.scene() gradient = QtGui.QLinearGradient(0, 0, 0, 1) gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode) gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68)) # Very Top Gradient gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30)) # Middle Gradient gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30)) # Middle Gradient gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68)) # Very Bottom Gradient scene.setBackgroundBrush(QtGui.QBrush(gradient)) # Resize and show the graph widget. graph.widget.resize(1600, 900) graph.widget.show() 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_())