Dynamically create context menu sub-folders based on folder structure of nodes.
This commit is contained in:
parent
76cb82acd9
commit
dc33aca6a0
Binary file not shown.
Binary file not shown.
@ -113,15 +113,15 @@ class OCRRegionWidget(QWidget):
|
|||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
pen = QPen(QColor(0, 0, 255))
|
pen = QPen(QColor(255, 255, 0)) # COLOR OF THE BOX ITSELF
|
||||||
pen.setWidth(3)
|
pen.setWidth(5) # WIDTH OF THE BOX BORDER
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
|
|
||||||
# Draw main rectangle
|
# Draw main rectangle
|
||||||
painter.drawRect(0, 0, self.width(), self.height())
|
painter.drawRect(0, 0, self.width(), self.height())
|
||||||
|
|
||||||
# Draw resize handles
|
# 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():
|
for handle in self._resize_handles():
|
||||||
painter.drawRect(handle)
|
painter.drawRect(handle)
|
||||||
|
|
||||||
|
@ -31,12 +31,49 @@ def data_api():
|
|||||||
"""
|
"""
|
||||||
return jsonify(get_data())
|
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():
|
def start_api_server():
|
||||||
"""
|
"""
|
||||||
Starts the Flask API server in a separate daemon thread.
|
Starts the Flask API server in a separate daemon thread.
|
||||||
"""
|
"""
|
||||||
def run():
|
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 = threading.Thread(target=run, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
116
borealis.py
116
borealis.py
@ -11,12 +11,9 @@ from Qt import QtWidgets, QtCore, QtGui
|
|||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# MONKEY-PATCH to fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
|
# 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:
|
try:
|
||||||
from qtpy.QtWidgets import QUndoStack
|
from qtpy.QtWidgets import QUndoStack
|
||||||
import qtpy
|
import qtpy
|
||||||
# Force QtGui.QUndoStack to reference QtWidgets.QUndoStack
|
|
||||||
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.")
|
||||||
@ -25,70 +22,42 @@ except ImportError:
|
|||||||
# Import your data_manager so we can start the Flask server
|
# Import your data_manager so we can start the Flask server
|
||||||
from Modules import data_manager
|
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 ---
|
# --- 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):
|
||||||
"""
|
"""
|
||||||
A robust patch that handles various call signatures for QGraphicsScene.setSelectionArea().
|
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:
|
try:
|
||||||
# First, try the original call with the given arguments.
|
|
||||||
return _original_setSelectionArea(self, *args, **kwargs)
|
return _original_setSelectionArea(self, *args, **kwargs)
|
||||||
except TypeError:
|
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:
|
if not args:
|
||||||
raise # If no args at all, we cannot fix it.
|
raise
|
||||||
|
painterPath = args[0]
|
||||||
painterPath = args[0] # QPainterPath
|
|
||||||
selection_op = QtCore.Qt.ReplaceSelection
|
selection_op = QtCore.Qt.ReplaceSelection
|
||||||
selection_mode = QtCore.Qt.IntersectsItemShape
|
selection_mode = QtCore.Qt.IntersectsItemShape
|
||||||
transform = QtGui.QTransform()
|
transform = QtGui.QTransform()
|
||||||
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 ROBUST PATCH FOR QGraphicsScene.setSelectionArea ---
|
# --- END PATCH ---
|
||||||
|
|
||||||
from OdenGraphQt import NodeGraph, BaseNode
|
from OdenGraphQt import NodeGraph, BaseNode
|
||||||
|
|
||||||
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.
|
||||||
and return a list of classes that subclass BaseNode.
|
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)
|
package = importlib.import_module(package_name)
|
||||||
|
|
||||||
# Get the root directory of the package
|
|
||||||
package_path = package.__path__[0]
|
package_path = package.__path__[0]
|
||||||
|
|
||||||
for root, _, files in os.walk(package_path):
|
for root, _, files in os.walk(package_path):
|
||||||
rel_path = os.path.relpath(root, package_path).replace(os.sep, '.')
|
rel_path = os.path.relpath(root, package_path).replace(os.sep, '.')
|
||||||
module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name
|
module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name
|
||||||
|
category_name = os.path.basename(root)
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith(".py") and file != "__init__.py":
|
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)
|
module = importlib.import_module(module_name)
|
||||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
|
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:
|
except Exception as e:
|
||||||
print(f"Failed to import {module_name}: {e}")
|
print(f"Failed to import {module_name}: {e}")
|
||||||
|
|
||||||
return imported_nodes
|
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.
|
||||||
For the Flyff Character Status Collector node, check if one already exists.
|
Ensures that only one FlyffCharacterStatusNode 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():
|
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"):
|
if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"):
|
||||||
for node in graph.all_nodes():
|
for node in graph.all_nodes():
|
||||||
if node.__class__.__name__ == "FlyffCharacterStatusNode":
|
if node.__class__.__name__ == "FlyffCharacterStatusNode":
|
||||||
# Show error message about duplicates
|
|
||||||
QtWidgets.QMessageBox.critical(
|
QtWidgets.QMessageBox.critical(
|
||||||
None,
|
None,
|
||||||
"Error",
|
"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
|
return
|
||||||
try:
|
try:
|
||||||
@ -131,68 +96,69 @@ def make_node_command(graph, node_type_str):
|
|||||||
QtWidgets.QMessageBox.critical(None, "Error", str(e))
|
QtWidgets.QMessageBox.critical(None, "Error", str(e))
|
||||||
|
|
||||||
def command():
|
def command():
|
||||||
# If there's already a QApplication running, just create the node now.
|
|
||||||
if QtWidgets.QApplication.instance():
|
if QtWidgets.QApplication.instance():
|
||||||
real_create()
|
real_create()
|
||||||
else:
|
else:
|
||||||
# Otherwise, schedule the node creation for the next event cycle.
|
|
||||||
QtCore.QTimer.singleShot(0, real_create)
|
QtCore.QTimer.singleShot(0, real_create)
|
||||||
|
|
||||||
return command
|
return command
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Create the QApplication first
|
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication([])
|
||||||
|
|
||||||
# Start the Flask server from data_manager so /data is always available
|
# Start Flask API Server
|
||||||
data_manager.start_api_server()
|
data_manager.start_api_server()
|
||||||
|
|
||||||
# Create the NodeGraph controller
|
# Create the NodeGraph
|
||||||
# (the monkey-patch ensures NodeGraph won't crash if it tries QtGui.QUndoStack(self))
|
|
||||||
graph = NodeGraph()
|
graph = NodeGraph()
|
||||||
graph.widget.setWindowTitle("Project Borealis - Workflow Automation System")
|
graph.widget.setWindowTitle("Project Borealis - Workflow Automation System")
|
||||||
|
|
||||||
# Dynamically import custom node classes from the 'Nodes' package.
|
# Dynamically import custom node classes from the 'Nodes' package.
|
||||||
custom_nodes = import_nodes_from_folder("Nodes")
|
custom_nodes_by_category = import_nodes_from_folder("Nodes")
|
||||||
for node_class in custom_nodes:
|
|
||||||
graph.register_node(node_class)
|
|
||||||
|
|
||||||
# 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")
|
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(
|
graph_context_menu.add_command(
|
||||||
"Remove Selected Node",
|
"Remove Selected Node",
|
||||||
lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None
|
lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Grid styling changes
|
# Grid styling changes
|
||||||
# 1) Dark background color
|
|
||||||
graph.set_background_color(20, 20, 20) # Dark gray
|
graph.set_background_color(20, 20, 20) # Dark gray
|
||||||
# 2) Subdued grid color
|
|
||||||
graph.set_grid_color(60, 60, 60) # Gray grid lines
|
graph.set_grid_color(60, 60, 60) # Gray grid lines
|
||||||
|
|
||||||
# Optionally, create a subtle gradient in the scene:
|
# Add gradient background
|
||||||
scene = 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)) # Very Top Gradient
|
gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68))
|
||||||
gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30)) # Middle Gradient
|
gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30))
|
||||||
gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30)) # Middle Gradient
|
gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30))
|
||||||
gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68)) # Very Bottom Gradient
|
gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68))
|
||||||
scene.setBackgroundBrush(QtGui.QBrush(gradient))
|
scene.setBackgroundBrush(QtGui.QBrush(gradient))
|
||||||
|
|
||||||
# Resize and show the graph widget.
|
# Resize and show the graph widget
|
||||||
graph.widget.resize(1600, 900)
|
graph.widget.resize(1600, 900)
|
||||||
graph.widget.show()
|
graph.widget.show()
|
||||||
|
|
||||||
|
# Global update function
|
||||||
def global_update():
|
def global_update():
|
||||||
for node in graph.all_nodes():
|
for node in graph.all_nodes():
|
||||||
if hasattr(node, "process_input"):
|
if hasattr(node, "process_input"):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user