diff --git a/QML/background_grid.qml b/QML/blueprint_grid.qml similarity index 100% rename from QML/background_grid.qml rename to QML/blueprint_grid.qml diff --git a/blueprint_grid.py b/blueprint_grid.py new file mode 100644 index 0000000..4c029ba --- /dev/null +++ b/blueprint_grid.py @@ -0,0 +1,193 @@ +#!/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_()) \ No newline at end of file diff --git a/borealis_transparent.py b/borealis_transparent.py index b8bc0a4..69ca9d1 100644 --- a/borealis_transparent.py +++ b/borealis_transparent.py @@ -1,98 +1,73 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys +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 +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsItem +from PyQt5.QtCore import Qt, QTimer, QRectF, QPointF +from PyQt5.QtGui import QColor, QPainter, QPen, QBrush, QGradient, QLinearGradient +from PyQt5 import QtWidgets, QtCore, QtGui -# 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.""" +# --- Custom Graph Scene --- +class CustomGraphScene(QGraphicsScene): + """ + Custom scene that draws a blueprint-style transparent grid with gradient shading. + """ def __init__(self, parent=None): - super().__init__("Enter Edit Mode", parent) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setStyleSheet("background-color: rgba(255,255,255,200); border: 1px solid black;") - self.resize(140, 40) + super().__init__(parent) + self.setBackgroundBrush(QtCore.Qt.transparent) + self.grid_color = QtGui.QColor(100, 160, 160, 160) # Blueprint grid color (10% more transparent) + self.grid_size = 115 -# Main Overlay Window + def drawBackground(self, painter, rect): + """ + Custom draw function to render a blueprint-style grid with gradient shading. + """ + painter.save() + painter.setRenderHint(QPainter.Antialiasing, False) + painter.setBrush(QtCore.Qt.NoBrush) # No background fill + pen = QPen(self.grid_color, 0.5) + + left = int(rect.left()) - (int(rect.left()) % self.grid_size) + top = int(rect.top()) - (int(rect.top()) % self.grid_size) + + # Draw vertical lines + lines = [] + for x in range(left, int(rect.right()), self.grid_size): + lines.append(QtCore.QLineF(x, rect.top(), x, rect.bottom())) + + # Draw horizontal lines + for y in range(top, int(rect.bottom()), self.grid_size): + lines.append(QtCore.QLineF(rect.left(), y, rect.right(), y)) + + painter.setPen(pen) + painter.drawLines(lines) + + # Draw gradient shading (top and bottom) + gradient = QLinearGradient(QPointF(rect.left(), rect.top()), QPointF(rect.left(), rect.bottom())) + gradient.setColorAt(0.0, QColor(0, 40, 100, 220)) # Darker blue at the top + gradient.setColorAt(0.5, QColor(0, 0, 0, 0)) # Transparent in the middle + gradient.setColorAt(1.0, QColor(0, 40, 100, 220)) # Darker blue at the bottom + painter.fillRect(rect, QBrush(gradient)) + + painter.restore() + +# --- Custom Graph View --- +class CustomGraphView(QGraphicsView): + """ + Custom view for the graph that applies full transparency. + """ + def __init__(self, scene, parent=None): + super().__init__(scene, parent) + 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) + +# --- Main Window --- class MainWindow(QMainWindow): - """A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle.""" + """A frameless, transparent overlay with a custom graph.""" def __init__(self): super().__init__() @@ -105,81 +80,28 @@ class MainWindow(QMainWindow): 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) + layout = QVBoxLayout(central) + layout.setContentsMargins(0, 0, 0, 0) 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 - self.editButton.setText("Exit Edit Mode") # Reflect that grid is active - - # 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}") + # Initialize Custom Graph Scene & View + self.scene = CustomGraphScene() + self.view = CustomGraphView(self.scene) + layout.addWidget(self.view) # Global update timer self.timer = QTimer(self) self.timer.timeout.connect(self.global_update) self.timer.start(500) - 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) - self.editButton.setText("Exit Edit Mode" if self.isEditMode else "Enter 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() + """Update all nodes periodically (to be implemented).""" + pass -# Entry Point +# --- Entry Point --- if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow()