193 lines
7.0 KiB
Python
193 lines
7.0 KiB
Python
#!/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_()) |