Added Functional Health Warning

This commit is contained in:
Nicole Rappe 2025-02-14 04:20:20 -07:00
parent 9db8cc6b19
commit d9623193d8
8 changed files with 223 additions and 97 deletions

View File

@ -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,13 +56,25 @@ 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)
@ -65,6 +82,30 @@ class CustomGraphView(QGraphicsView):
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):
"""A frameless, transparent overlay with a custom graph."""
@ -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__':

View File

@ -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_())

View File

@ -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))
# 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

Binary file not shown.

View File

@ -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: