#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import pkgutil import importlib import inspect import types from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt5.QtGui import QGuiApplication from PyQt5.QtQuick import QQuickView # OdenGraphQt Fix: Monkey-patch QUndoStack import OdenGraphQt.base.graph as base_graph from PyQt5 import QtWidgets base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack import OdenGraphQt.base.commands as base_commands _original_redo = base_commands.NodesRemovedCmd.redo _original_undo = base_commands.NodesRemovedCmd.undo def _patched_redo(self): try: _original_redo(self) except TypeError as e: if "unexpected type" in str(e) and hasattr(self, 'node'): node_ids = [] if isinstance(self.node, list): node_ids = [getattr(n, 'id', str(n)) for n in self.node] else: node_ids = [getattr(self.node, 'id', str(self.node))] self.graph.nodes_deleted.emit(node_ids) else: raise def _patched_undo(self): try: _original_undo(self) except TypeError as e: if "unexpected type" in str(e) and hasattr(self, 'node'): node_ids = [] if isinstance(self.node, list): node_ids = [getattr(n, 'id', str(n)) for n in self.node] else: node_ids = [getattr(self.node, 'id', str(self.node))] self.graph.nodes_deleted.emit(node_ids) else: raise base_commands.NodesRemovedCmd.redo = _patched_redo base_commands.NodesRemovedCmd.undo = _patched_undo # OdenGraphQt Transparent Viewer from OdenGraphQt.widgets.viewer import NodeViewer class TransparentViewer(NodeViewer): """A NodeViewer that does not paint anything in drawBackground() -> Fully transparent.""" def drawBackground(self, painter, rect): pass # Do nothing, ensuring transparency. # NodeGraph & Node Import Helpers from OdenGraphQt import NodeGraph, BaseNode 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 def make_node_command(graph, node_type): def command(): try: graph.create_node(node_type) except Exception as e: print(f"Error creating node of type {node_type}: {e}") return command # Edit Mode Button class EditButton(QPushButton): """A small, frameless button to toggle edit mode.""" def __init__(self, parent=None): super().__init__("Toggle Edit Mode", parent) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) # Dark gray background with white text. self.setStyleSheet("background-color: #444444; border: 1px solid black; color: white;") self.resize(140, 40) # Main Overlay Window class MainWindow(QMainWindow): """A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle.""" def __init__(self): super().__init__() # Full-screen overlay app = QApplication.instance() screen_geo = app.primaryScreen().geometry() self.setGeometry(screen_geo) # Frameless, top-most, fully transparent self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_TranslucentBackground, True) # QML Background self.qml_view = QQuickView() self.qml_view.setSource(QUrl("qml/background_grid.qml")) self.qml_view.setFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.qml_view.setClearBeforeRendering(True) self.qml_view.setColor(Qt.transparent) self.qml_view.show() # Save the QML root object for later property sync self.qml_root = self.qml_view.rootObject() # NodeGraph with TransparentViewer self.graph = NodeGraph(viewer=TransparentViewer()) self.nodeGraphWidget = self.graph.widget self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;") # Transparent central widget central = QWidget(self) central.setAttribute(Qt.WA_TranslucentBackground, True) self.setCentralWidget(central) self.nodeGraphWidget.setParent(central) self.nodeGraphWidget.setGeometry(central.rect()) # Edit Mode Button (Python controlled) self.editButton = EditButton(self) self.editButton.move(10, 10) self.editButton.clicked.connect(self.toggleEditMode) self.isEditMode = True # Set edit mode enabled by default # Ensure QML grid overlay is enabled at startup if self.qml_root: self.qml_root.setProperty("editMode", self.isEditMode) # Import custom nodes try: custom_nodes = import_nodes_from_folder('Nodes') for node_class in custom_nodes: self.graph.register_node(node_class) graph_menu = self.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_menu.add_command( f"Add {node_name}", make_node_command(self.graph, node_type) ) except Exception as e: print(f"Error setting up custom nodes: {e}") # Global update timer self.timer = QTimer(self) self.timer.timeout.connect(self.global_update) self.timer.start(500) # Timer to ensure the button stays on top (hacky, but effective) self.raiseTimer = QTimer(self) self.raiseTimer.timeout.connect(self.editButton.raise_) self.raiseTimer.start(1000) # Raise the button every 1 second self.show() self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) def toggleEditMode(self): """Toggle edit mode (pass-through clicks vs interactive).""" self.isEditMode = not self.isEditMode self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) # Button text remains constant. self.editButton.setText("Toggle Edit Mode") if self.qml_root: self.qml_root.setProperty("editMode", self.isEditMode) def global_update(self): """Update all nodes periodically.""" for node in self.graph.all_nodes(): if hasattr(node, "process_input"): node.process_input() # Entry Point if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())