diff --git a/QML/blueprint_grid.qml b/Experiments/Transparent Nodes/QML/blueprint_grid.qml similarity index 100% rename from QML/blueprint_grid.qml rename to Experiments/Transparent Nodes/QML/blueprint_grid.qml diff --git a/blueprint_grid.py b/Experiments/Transparent Nodes/blueprint_grid.py similarity index 100% rename from blueprint_grid.py rename to Experiments/Transparent Nodes/blueprint_grid.py diff --git a/borealis_transparent.py b/Experiments/Transparent Nodes/borealis_transparent.py similarity index 61% rename from borealis_transparent.py rename to Experiments/Transparent Nodes/borealis_transparent.py index 69ca9d1..0560967 100644 --- a/borealis_transparent.py +++ b/Experiments/Transparent Nodes/borealis_transparent.py @@ -2,10 +2,15 @@ import pkgutil import importlib import inspect -from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsItem +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu from PyQt5.QtCore import Qt, QTimer, QRectF, QPointF from PyQt5.QtGui import QColor, QPainter, QPen, QBrush, QGradient, QLinearGradient from PyQt5 import QtWidgets, QtCore, QtGui +from OdenGraphQt import NodeGraph, BaseNode + +# --- Fix Missing QUndoStack in QtGui --- +import OdenGraphQt.base.graph as base_graph +base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack # Monkey-patch the missing QUndoStack # --- Custom Graph Scene --- class CustomGraphScene(QGraphicsScene): @@ -51,19 +56,55 @@ class CustomGraphScene(QGraphicsScene): painter.restore() +# --- Node Management --- +def import_nodes_from_folder(package_name): + imported_nodes = [] + package = importlib.import_module(package_name) + for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__, 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) + return imported_nodes + # --- Custom Graph View --- class CustomGraphView(QGraphicsView): """ - Custom view for the graph that applies full transparency. + Custom view for the graph that applies full transparency and handles right-click context menu. """ - def __init__(self, scene, parent=None): + def __init__(self, scene, graph, parent=None): super().__init__(scene, parent) + self.graph = graph # Reference to NodeGraph self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setStyleSheet("background: transparent; border: none;") self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + # Enable context menu on right-click + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_context_menu) + + def show_context_menu(self, position): + """ + Displays the node creation context menu with dynamically loaded nodes. + """ + menu = QMenu() + for node_class in self.graph.registered_nodes(): + node_name = getattr(node_class, "NODE_NAME", node_class.__name__) + menu.addAction(f"Create {node_name}", lambda nc=node_class: self.create_node(nc)) + menu.exec_(self.mapToGlobal(position)) + + def create_node(self, node_class): + """ + Creates a node instance of the given class in the NodeGraph. + """ + try: + node = self.graph.create_node(f"{node_class.__identifier__}.{node_class.__name__}") + print(f"Created node: {node_class.__name__}") + except Exception as e: + print(f"Error creating node: {e}") # --- Main Window --- class MainWindow(QMainWindow): @@ -87,9 +128,17 @@ class MainWindow(QMainWindow): layout.setContentsMargins(0, 0, 0, 0) self.setCentralWidget(central) + # Initialize NodeGraph + self.graph = NodeGraph() + + # Load custom nodes + custom_nodes = import_nodes_from_folder('Nodes') + for node_class in custom_nodes: + self.graph.register_node(node_class) + # Initialize Custom Graph Scene & View self.scene = CustomGraphScene() - self.view = CustomGraphView(self.scene) + self.view = CustomGraphView(self.scene, self.graph, self) layout.addWidget(self.view) # Global update timer @@ -98,8 +147,10 @@ class MainWindow(QMainWindow): self.timer.start(500) def global_update(self): - """Update all nodes periodically (to be implemented).""" - pass + """Update all nodes periodically.""" + for node in self.graph.all_nodes(): + if hasattr(node, "process_input"): + node.process_input() # --- Entry Point --- if __name__ == '__main__': diff --git a/Modules/overlay_helpers.py b/Modules/overlay_helpers.py deleted file mode 100644 index 4ce2665..0000000 --- a/Modules/overlay_helpers.py +++ /dev/null @@ -1,80 +0,0 @@ -import sys -from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtCore import Qt, QRect, QPoint -from PyQt5.QtGui import QPainter, QPen, QColor, QFont, QFontMetrics - -class OverlayCanvas(QWidget): - """ - UI overlay for drawing and interacting with on-screen elements. - """ - def __init__(self, parent=None): - super().__init__(parent) - - # **Full-screen overlay** - screen_geo = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) # Set to full screen - - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - self.setAttribute(Qt.WA_NoSystemBackground, True) - self.setAttribute(Qt.WA_OpaquePaintEvent, False) - self.setAttribute(Qt.WA_AlwaysStackOnTop, True) - - # **Helper Object: Low Health Alert** - self.helper_LowHealthAlert = QRect(250, 300, 900, 35) # Adjusted width and height - self.dragging = False - self.resizing = False - self.drag_offset = QPoint() - - def paintEvent(self, event): - """Draw the helper overlay objects.""" - painter = QPainter(self) - painter.setPen(Qt.NoPen) - painter.setBrush(QColor(255, 0, 0)) # Solid red rectangle - painter.drawRect(self.helper_LowHealthAlert) - - # Draw bold white text centered within the rectangle - font = QFont("Arial", 14, QFont.Bold) # Scaled text - painter.setFont(font) - painter.setPen(QColor(255, 255, 255)) - - text = "LOW HEALTH" - metrics = QFontMetrics(font) - text_width = metrics.horizontalAdvance(text) - text_height = metrics.height() - text_x = self.helper_LowHealthAlert.center().x() - text_width // 2 - text_y = self.helper_LowHealthAlert.center().y() + text_height // 4 - - painter.drawText(text_x, text_y, text) - - def mousePressEvent(self, event): - """Detect clicks for dragging and resizing the helper object.""" - if event.button() == Qt.LeftButton: - if self.helper_LowHealthAlert.contains(event.pos()): - if event.pos().x() > self.helper_LowHealthAlert.right() - 10 and event.pos().y() > self.helper_LowHealthAlert.bottom() - 10: - self.resizing = True - else: - self.dragging = True - self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft() - - def mouseMoveEvent(self, event): - """Handle dragging and resizing movements.""" - if self.dragging: - self.helper_LowHealthAlert.moveTopLeft(event.pos() - self.drag_offset) - self.update() - elif self.resizing: - new_width = max(150, event.pos().x() - self.helper_LowHealthAlert.x()) - new_height = max(20, event.pos().y() - self.helper_LowHealthAlert.y()) - self.helper_LowHealthAlert.setSize(new_width, new_height) - self.update() - - def mouseReleaseEvent(self, event): - """End dragging or resizing event.""" - self.dragging = False - self.resizing = False - -if __name__ == '__main__': - app_gui = QApplication(sys.argv) - overlay_window = OverlayCanvas() - overlay_window.show() - sys.exit(app_gui.exec_()) diff --git a/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc b/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc index 35e9fd0..cca471d 100644 Binary files a/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc and b/Nodes/__pycache__/flyff_low_health_alert_node.cpython-312.pyc differ diff --git a/Nodes/flyff_low_health_alert_node.py b/Nodes/flyff_low_health_alert_node.py index 708c69e..2aa0b1d 100644 --- a/Nodes/flyff_low_health_alert_node.py +++ b/Nodes/flyff_low_health_alert_node.py @@ -1,22 +1,177 @@ +import time +import sys from OdenGraphQt import BaseNode -from OdenGraphQt.constants import NodePropWidgetEnum -from OdenGraphQt.widgets.node_widgets import NodeLineEditValidatorCheckBox +from Qt import QtWidgets, QtCore, QtGui -class CheckboxNode(BaseNode): +# Attempt to import winsound (Windows-only) +try: + import winsound + HAS_WINSOUND = True +except ImportError: + HAS_WINSOUND = False + + +class OverlayCanvas(QtWidgets.QWidget): + """ + UI overlay for displaying a red warning box, which can be repositioned by dragging. + """ + + def __init__(self, parent=None): + super().__init__(parent) + + # **Full-screen overlay** + screen_geo = QtWidgets.QApplication.primaryScreen().geometry() + self.setGeometry(screen_geo) # Set to full screen + + self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) + self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False) + self.setAttribute(QtCore.Qt.WA_AlwaysStackOnTop, True) + + # **Draggable Low Health Alert** + self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35) # Default Position + self.dragging = False + self.drag_offset = QtCore.QPoint() + + self.setVisible(False) # Initially hidden + + def paintEvent(self, event): + """Draw the helper overlay objects.""" + if not self.isVisible(): + return # Don't draw anything if invisible + + painter = QtGui.QPainter(self) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(QtGui.QColor(255, 0, 0)) # Solid red rectangle + painter.drawRect(self.helper_LowHealthAlert) + + # Draw bold white text centered within the rectangle + font = QtGui.QFont("Arial", 14, QtGui.QFont.Bold) + painter.setFont(font) + painter.setPen(QtGui.QColor(255, 255, 255)) + + text = "LOW HEALTH" + metrics = QtGui.QFontMetrics(font) + text_width = metrics.horizontalAdvance(text) + text_height = metrics.height() + text_x = self.helper_LowHealthAlert.center().x() - text_width // 2 + text_y = self.helper_LowHealthAlert.center().y() + text_height // 4 + + painter.drawText(text_x, text_y, text) + + def toggle_alert(self, state): + """ + Show or hide the overlay based on the state (1 = show, 0 = hide). + """ + self.setVisible(state == 1) + self.update() + + def mousePressEvent(self, event): + """Detect clicks inside the red box and allow dragging.""" + if event.button() == QtCore.Qt.LeftButton and self.helper_LowHealthAlert.contains(event.pos()): + self.dragging = True + self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft() + + def mouseMoveEvent(self, event): + """Handle dragging movement.""" + if self.dragging: + new_x = event.pos().x() - self.drag_offset.x() + new_y = event.pos().y() - self.drag_offset.y() + self.helper_LowHealthAlert.moveTopLeft(QtCore.QPoint(new_x, new_y)) + self.update() + + def mouseReleaseEvent(self, event): + """Stop dragging when the mouse button is released.""" + self.dragging = False + + +class FlyffLowHealthAlertNode(BaseNode): + """ + Custom OdenGraphQt node that toggles a visual alert overlay and plays a beep when health is low. + """ - # set a unique node identifier. __identifier__ = 'bunny-lab.io.flyff_low_health_alert_node' - - # set the initial default node name. NODE_NAME = 'Flyff - Low Health Alert' - def __init__(self): - super(CheckboxNode, self).__init__() + overlay_instance = None # Shared overlay instance + last_beep_time = 0 # Time tracking for beep interval + BEEP_INTERVAL_SECONDS = 2 # Beep every 2 seconds - # Create checkboxes to decide which kind of alert(s) to utilize. + def __init__(self): + super(FlyffLowHealthAlertNode, self).__init__() + + # Create checkboxes to decide which kind of alert(s) to utilize self.add_checkbox('cb_1', '', 'Sound Alert', True) self.add_checkbox('cb_2', '', 'Visual Alert', True) # Create Input Port self.add_input('Toggle (1 = On | 0 = Off)', color=(200, 100, 0)) - #self.add_output('out', color=(0, 100, 200)) \ No newline at end of file + + # Add text input widget to display received value + self.add_text_input('value', 'Current Value', text='0') + + # Ensure only one overlay instance exists + if not FlyffLowHealthAlertNode.overlay_instance: + FlyffLowHealthAlertNode.overlay_instance = OverlayCanvas() + FlyffLowHealthAlertNode.overlay_instance.show() + + def process_input(self): + """ + This function runs every 500ms (via the global update loop). + It updates the displayed value and toggles the alert if needed. + """ + input_port = self.input(0) + + # If there is a connected node, fetch its output value + if input_port.connected_ports(): + connected_node = input_port.connected_ports()[0].node() + if hasattr(connected_node, 'get_property'): + value = connected_node.get_property('value') + else: + value = "0" + else: + value = "0" # Default to zero if nothing is connected + + try: + input_value = int(value) # Ensure we interpret input as an integer (0 or 1) + except ValueError: + input_value = 0 # Default to off if the input is not valid + + # Update the value display box + self.set_property('value', str(input_value)) + + # Check if the "Visual Alert" checkbox is enabled + visual_alert_enabled = self.get_property('cb_2') + + # Ensure that if "Visual Alert" is unchecked, the overlay is always hidden + if not visual_alert_enabled: + FlyffLowHealthAlertNode.overlay_instance.toggle_alert(0) + else: + FlyffLowHealthAlertNode.overlay_instance.toggle_alert(input_value) + + # Check if "Sound Alert" is enabled and beep if necessary + self.handle_beep(input_value) + + def handle_beep(self, input_value): + """ + Plays a beep sound every 2 seconds when the value is `1` and "Sound Alert" is enabled. + """ + sound_alert_enabled = self.get_property('cb_1') + current_time = time.time() + + if input_value == 1 and sound_alert_enabled: + if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= FlyffLowHealthAlertNode.BEEP_INTERVAL_SECONDS: + FlyffLowHealthAlertNode.last_beep_time = current_time + self.play_beep() + else: + FlyffLowHealthAlertNode.last_beep_time = 0 # Reset when health is safe + + def play_beep(self): + """ + Plays a beep using `winsound.Beep` (Windows) or prints a terminal bell (`\a`). + """ + if HAS_WINSOUND: + winsound.Beep(376, 100) # 376 Hz, 100ms duration + else: + print('\a', end='') # Terminal bell for non-Windows systems diff --git a/Project_Borealis.zip b/Project_Borealis.zip deleted file mode 100644 index 22db142..0000000 Binary files a/Project_Borealis.zip and /dev/null differ diff --git a/borealis.py b/borealis.py index dad0a31..1f8eb20 100644 --- a/borealis.py +++ b/borealis.py @@ -88,7 +88,7 @@ if __name__ == '__main__': ) # Resize and show the graph widget. - graph.widget.resize(1200, 800) + graph.widget.resize(1920, 1080) graph.widget.show() # Global update timer: