Dynamically create context menu sub-folders based on folder structure of nodes.

This commit is contained in:
Nicole Rappe 2025-02-24 18:33:04 -07:00
parent 76cb82acd9
commit dc33aca6a0
11 changed files with 82 additions and 79 deletions

View File

@ -113,15 +113,15 @@ class OCRRegionWidget(QWidget):
def paintEvent(self, event):
painter = QPainter(self)
pen = QPen(QColor(0, 0, 255))
pen.setWidth(3)
pen = QPen(QColor(255, 255, 0)) # COLOR OF THE BOX ITSELF
pen.setWidth(5) # WIDTH OF THE BOX BORDER
painter.setPen(pen)
# Draw main rectangle
painter.drawRect(0, 0, self.width(), self.height())
# Draw resize handles
painter.setBrush(QColor(0, 0, 255))
painter.setBrush(QColor(255, 255, 0)) # COLOR OF THE RESIZE HANDLES
for handle in self._resize_handles():
painter.drawRect(handle)

View File

@ -31,12 +31,49 @@ def data_api():
"""
return jsonify(get_data())
@app.route('/exp')
def exp_api():
"""
Returns the EXP data.
"""
return jsonify({"exp": get_data()["exp"]})
@app.route('/hp')
def hp_api():
"""
Returns the HP data.
"""
return jsonify({
"hp_current": get_data()["hp_current"],
"hp_total": get_data()["hp_total"]
})
@app.route('/mp')
def mp_api():
"""
Returns the MP data.
"""
return jsonify({
"mp_current": get_data()["mp_current"],
"mp_total": get_data()["mp_total"]
})
@app.route('/fp')
def fp_api():
"""
Returns the FP data.
"""
return jsonify({
"fp_current": get_data()["fp_current"],
"fp_total": get_data()["fp_total"]
})
def start_api_server():
"""
Starts the Flask API server in a separate daemon thread.
"""
def run():
app.run(host="127.0.0.1", port=5000)
app.run(host="0.0.0.0", port=5000) # Allows external connections
t = threading.Thread(target=run, daemon=True)
t.start()

View File

@ -11,12 +11,9 @@ 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.")
@ -25,70 +22,42 @@ except ImportError:
# 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
raise
painterPath = args[0]
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 ---
# --- END PATCH ---
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.
Recursively import all modules from the given package.
Returns a dictionary where keys are subfolder names, and values are lists of BaseNode subclasses.
"""
imported_nodes = []
nodes_by_category = {}
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
category_name = os.path.basename(root)
for file in files:
if file.endswith(".py") and file != "__init__.py":
@ -97,31 +66,27 @@ def import_nodes_from_folder(package_name):
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)
if category_name not in nodes_by_category:
nodes_by_category[category_name] = []
nodes_by_category[category_name].append(obj)
except Exception as e:
print(f"Failed to import {module_name}: {e}")
return imported_nodes
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.
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.
Ensures that only one FlyffCharacterStatusNode exists.
"""
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."
"Only one Flyff Character Status Collector node is allowed."
)
return
try:
@ -131,68 +96,69 @@ def make_node_command(graph, node_type_str):
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
# Start Flask API Server
data_manager.start_api_server()
# Create the NodeGraph controller
# (the monkey-patch ensures NodeGraph won't crash if it tries QtGui.QUndoStack(self))
# Create the NodeGraph
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)
custom_nodes_by_category = import_nodes_from_folder("Nodes")
# Add context menu commands for dynamic node creation.
# 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 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.
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
)
# 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:
# 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)) # 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
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.
# 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"):