From 17f3be4c47b9e91f0597764db4333e2bca7bc78c Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Tue, 25 Feb 2025 17:20:31 -0700 Subject: [PATCH] Added Status bar, menu bar, and made context menu blue. --- Experiments/gui_elements.py | 98 +++++++++ borealis.py | 399 ++++++++++++++++++++++-------------- 2 files changed, 340 insertions(+), 157 deletions(-) create mode 100644 Experiments/gui_elements.py diff --git a/Experiments/gui_elements.py b/Experiments/gui_elements.py new file mode 100644 index 0000000..f65f513 --- /dev/null +++ b/Experiments/gui_elements.py @@ -0,0 +1,98 @@ +# example_qt_interface.py +import sys +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QIcon +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, + QLabel, QMenuBar, QToolBar, QSplitter, QListWidget, + QTextEdit, QStatusBar, QFileDialog, QPushButton +) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("Example Qt Interface") + + # Create and set up the menu bar. + menu_bar = QMenuBar(self) + self.setMenuBar(menu_bar) + + # File menu. + file_menu = menu_bar.addMenu("File") + + # Create some actions to populate the File menu. + open_action = QAction("Open", self) + open_action.triggered.connect(self.open_file) + file_menu.addAction(open_action) + + save_action = QAction("Save", self) + save_action.triggered.connect(self.save_file) + file_menu.addAction(save_action) + + exit_action = QAction("Exit", self) + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # Create a toolbar and add some actions. + tool_bar = QToolBar("Main Toolbar", self) + tool_bar.addAction(open_action) + tool_bar.addAction(save_action) + self.addToolBar(Qt.TopToolBarArea, tool_bar) + + # Set up a status bar at the bottom. + self.setStatusBar(QStatusBar(self)) + self.statusBar().showMessage("Ready") + + # Create your central widget area. + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + + # A splitter as an example container that can hold multiple widgets side-by-side. + splitter = QSplitter() + + # Left side: a simple list widget. + self.list_widget = QListWidget() + self.list_widget.addItem("Item A") + self.list_widget.addItem("Item B") + self.list_widget.addItem("Item C") + splitter.addWidget(self.list_widget) + + # Right side: a text edit widget. + self.text_edit = QTextEdit() + self.text_edit.setPlainText("Type here...") + splitter.addWidget(self.text_edit) + + layout.addWidget(splitter) + + # Example button in the central widget area. + example_button = QPushButton("Click Me") + example_button.clicked.connect(self.on_button_clicked) + layout.addWidget(example_button) + + def open_file(self): + file_name, _ = QFileDialog.getOpenFileName(self, "Open File", "", "All Files (*.*)") + if file_name: + self.statusBar().showMessage(f"Opened: {file_name}") + + def save_file(self): + file_name, _ = QFileDialog.getSaveFileName(self, "Save File", "", "All Files (*.*)") + if file_name: + self.statusBar().showMessage(f"Saved: {file_name}") + + def on_button_clicked(self): + self.statusBar().showMessage("Button clicked!") + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.resize(800, 600) + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/borealis.py b/borealis.py index 97b2fd3..22517fb 100644 --- a/borealis.py +++ b/borealis.py @@ -2,10 +2,9 @@ #!/usr/bin/env python3 import sys -import pkgutil -import importlib -import inspect import os +import inspect +import importlib from Qt import QtWidgets, QtCore, QtGui @@ -21,7 +20,6 @@ except ImportError: # ------------------------------------------------------------------ # MONKEY-PATCH to fix "'BackdropNodeItem' object has no attribute 'widgets'" -# by giving BackdropNodeItem a trivial widgets dictionary. try: from OdenGraphQt.nodes.backdrop_node import BackdropNodeItem if not hasattr(BackdropNodeItem, "widgets"): @@ -32,6 +30,7 @@ except ImportError: # Import your data_manager so we can start the Flask server from Modules import data_manager +data_manager.start_api_server() # --- BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea --- _original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea @@ -57,10 +56,68 @@ QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea from OdenGraphQt import NodeGraph, BaseNode from OdenGraphQt.widgets.dialogs import FileDialog + +def ensure_workflows_folder(): + """ + Ensures a 'Workflows' subfolder exists. + """ + if not os.path.exists("Workflows"): + os.makedirs("Workflows") + + +def close_workflow(graph: NodeGraph): + """ + Closes the current workflow (removes all nodes and connections). + """ + graph.clear_session() + + +def save_workflow(graph: NodeGraph): + """ + Saves the current workflow (including custom names, positions, wires, etc.) + into a JSON file in the 'Workflows' subfolder. + """ + ensure_workflows_folder() + file_filter = "JSON Files (*.json);;All Files (*.*)" + dlg = FileDialog.getSaveFileName(None, "Save Workflow", os.path.join("Workflows", ""), file_filter) + file_path = dlg[0] + if not file_path: + return # User canceled + + if not file_path.lower().endswith(".json"): + file_path += ".json" + + try: + graph.save_session(file_path) + print(f"Workflow saved to {file_path}") + except Exception as e: + QtWidgets.QMessageBox.critical(None, "Error Saving Workflow", str(e)) + + +def load_workflow(graph: NodeGraph): + """ + Loads a workflow (including node values, connections, positions, etc.) + from a specified JSON file. + """ + ensure_workflows_folder() + file_filter = "JSON Files (*.json);;All Files (*.*)" + dlg = FileDialog.getOpenFileName(None, "Load Workflow", os.path.join("Workflows", ""), file_filter) + file_path = dlg[0] + if not file_path: + return # User canceled + + try: + graph.load_session(file_path) + print(f"Workflow loaded from {file_path}") + except Exception as e: + QtWidgets.QMessageBox.critical(None, "Error Loading Workflow", str(e)) + + def import_nodes_from_folder(package_name): """ Recursively import all modules from the given package. - Returns a dictionary where keys are subfolder names, and values are lists of BaseNode subclasses. + Returns a dictionary where keys are subfolder names, + and values are lists of BaseNode subclasses. """ nodes_by_category = {} package = importlib.import_module(package_name) @@ -83,13 +140,14 @@ def import_nodes_from_folder(package_name): nodes_by_category[category_name].append(obj) except Exception as e: print(f"Failed to import {module_name}: {e}") - + return nodes_by_category + def make_node_command(graph, node_type_str): """ Return a function that creates a node of the given type at the current cursor position. - Ensures that only one FlyffCharacterStatusNode exists. + Ensures that only one FlyffCharacterStatusNode exists if that's the node being created. """ def real_create(): if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"): @@ -115,161 +173,188 @@ def make_node_command(graph, node_type_str): return command -def ensure_workflows_folder(): - """ - Ensures a 'Workflows' subfolder exists. - """ - if not os.path.exists("Workflows"): - os.makedirs("Workflows") -def close_workflow(graph: NodeGraph): - """ - Closes the current workflow (removes all nodes and connections). - """ - graph.clear_session() +class BorealisWindow(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() -def save_workflow(graph: NodeGraph): - """ - Saves the current workflow (including custom names, positions, wires, etc.) into a JSON file - in the 'Workflows' subfolder. - """ - ensure_workflows_folder() - file_filter = "JSON Files (*.json);;All Files (*.*)" - dlg = FileDialog.getSaveFileName(None, "Save Workflow", os.path.join("Workflows", ""), file_filter) - file_path = dlg[0] - if not file_path: - return # User canceled + self.setWindowTitle("Project Borealis - Workflow Automation System") - if not file_path.lower().endswith(".json"): - file_path += ".json" + # Create NodeGraph and widget + self.graph = NodeGraph() - try: - graph.save_session(file_path) - print(f"Workflow saved to {file_path}") - except Exception as e: - QtWidgets.QMessageBox.critical(None, "Error Saving Workflow", str(e)) + # Grid styling changes + self.graph.set_background_color(20, 20, 20) # Dark gray + self.graph.set_grid_color(60, 60, 60) # Gray grid lines -def load_workflow(graph: NodeGraph): - """ - Loads a workflow (including node values, connections, positions, etc.) from a specified JSON file. - """ - ensure_workflows_folder() - file_filter = "JSON Files (*.json);;All Files (*.*)" - dlg = FileDialog.getOpenFileName(None, "Load Workflow", os.path.join("Workflows", ""), file_filter) - file_path = dlg[0] - if not file_path: - return # User canceled + # Add gradient background + scene = self.graph.scene() + gradient = QtGui.QLinearGradient(0, 0, 0, 1) + gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode) + gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68)) + gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30)) + gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30)) + gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68)) + scene.setBackgroundBrush(QtGui.QBrush(gradient)) + + # Load custom nodes from "Nodes" folder + custom_nodes_by_category = import_nodes_from_folder("Nodes") + for category, node_classes in custom_nodes_by_category.items(): + for node_class in node_classes: + self.graph.register_node(node_class) + + # Build the right-click context menu for the graph + self._build_graph_context_menu(custom_nodes_by_category) + + # Setup the central widget to show the NodeGraph + self.setCentralWidget(self.graph.widget) + + # Build the top menu bar + self._build_menubar() + + # Create a status bar + self.setStatusBar(QtWidgets.QStatusBar(self)) + # Default status message + self.update_status_bar("Flask Server: http://0.0.0.0:5000/data") + + # Resize + self.resize(1200, 800) + + def _build_graph_context_menu(self, custom_nodes_by_category): + """ + Build context menu and re-apply the custom stylesheet for a + 'blue-ish' highlight, removing the pink/purple highlight. + """ + # Grab the node graph's context menu + graph_context_menu = self.graph.get_context_menu("graph") + + # We can define a custom style for the QMenu objects: + menu_stylesheet = """ + QMenu { + background-color: rgb(30, 30, 30); + border: 1px solid rgba(200, 200, 200, 60); + } + QMenu::item { + padding: 5px 18px 2px; + background-color: transparent; + } + QMenu::item:selected { + color: rgb(255, 255, 255); + background-color: rgba(60, 120, 180, 150); + } + QMenu::separator { + height: 1px; + background: rgba(255, 255, 255, 50); + margin: 4px 8px; + } + """ + + # Apply the custom style + if graph_context_menu and graph_context_menu.qmenu: + graph_context_menu.qmenu.setStyleSheet(menu_stylesheet) + + # Top-level "Add Nodes" folder in the context menu + add_nodes_menu = graph_context_menu.add_menu("Add Nodes") + + # If you want the same style for "Add Nodes" submenus: + if add_nodes_menu and add_nodes_menu.qmenu: + add_nodes_menu.qmenu.setStyleSheet(menu_stylesheet) + + # For each category, build a submenu under "Add Nodes" + for category, node_classes in custom_nodes_by_category.items(): + category_menu = add_nodes_menu.add_menu(category) + # Also reapply style for each new sub-menu: + if category_menu and category_menu.qmenu: + category_menu.qmenu.setStyleSheet(menu_stylesheet) + + for node_class in node_classes: + node_type = f"{node_class.__identifier__}.{node_class.__name__}" + node_name = node_class.NODE_NAME + category_menu.add_command( + node_name, + make_node_command(self.graph, node_type) + ) + + # Provide a way to remove selected nodes + graph_context_menu.add_command( + "Remove Selected Node", + lambda: [self.graph.remove_node(node) + for node in self.graph.selected_nodes()] + if self.graph.selected_nodes() else None + ) + + # No 'Workflows' portion here because we moved it into the top menubar. + + def _build_menubar(self): + menubar = self.menuBar() + + # 1) Workflows menu in menubar + workflows_menu = menubar.addMenu("Workflows") + + load_action = QtWidgets.QAction("Load Workflow", self) + load_action.triggered.connect(lambda: load_workflow(self.graph)) + workflows_menu.addAction(load_action) + + save_action = QtWidgets.QAction("Save Workflow", self) + save_action.triggered.connect(lambda: save_workflow(self.graph)) + workflows_menu.addAction(save_action) + + close_action = QtWidgets.QAction("Close Workflow", self) + close_action.triggered.connect(lambda: close_workflow(self.graph)) + workflows_menu.addAction(close_action) + + # 2) About menu + about_menu = menubar.addMenu("About") + + # "Gitea Project" option + gitea_action = QtWidgets.QAction("Gitea Project", self) + gitea_action.triggered.connect(self._open_gitea_project) + about_menu.addAction(gitea_action) + + # "Credits" option + credits_action = QtWidgets.QAction("Credits", self) + credits_action.triggered.connect(self._show_credits_popup) + about_menu.addAction(credits_action) + + # "Check for Updates" option + updates_action = QtWidgets.QAction("Check for Updates", self) + updates_action.triggered.connect(self._show_updates_popup) + about_menu.addAction(updates_action) + + def _open_gitea_project(self): + url = QtCore.QUrl("https://git.bunny-lab.io/Scripts/Project_Borealis") + QtGui.QDesktopServices.openUrl(url) + + def _show_credits_popup(self): + QtWidgets.QMessageBox.information( + self, + "Credits", + "Created by Nicole Rappe" + ) + + def _show_updates_popup(self): + QtWidgets.QMessageBox.information( + self, + "Check for Updates", + "Built-in update functionality has not been built yet, but it's on the roadmap. Stay tuned." + ) + + def update_status_bar(self, data: str): + """ + Flattens multi-line data into a single line + (using ' | ' to replace newlines) + and shows it in the status bar. + """ + flattened = data.replace("\n", " | ") + self.statusBar().showMessage(flattened) + + +def main(): + app = QtWidgets.QApplication(sys.argv) + window = BorealisWindow() + window.show() + sys.exit(app.exec_()) - try: - graph.load_session(file_path) - print(f"Workflow loaded from {file_path}") - except Exception as e: - QtWidgets.QMessageBox.critical(None, "Error Loading Workflow", str(e)) if __name__ == "__main__": - app = QtWidgets.QApplication([]) - - # Start Flask API Server - data_manager.start_api_server() - - # Create the NodeGraph - graph = NodeGraph() - graph.widget.setWindowTitle("Project Borealis - Workflow Automation System") - - # Dynamically import custom node classes from the 'Nodes' package. - custom_nodes_by_category = import_nodes_from_folder("Nodes") - - # Register each node in its category - for category, node_classes in custom_nodes_by_category.items(): - for node_class in node_classes: - graph.register_node(node_class) - - # Create root context menu - graph_context_menu = graph.get_context_menu("graph") - - # Create a top-level "Add Nodes" folder in the context menu - add_nodes_menu = graph_context_menu.add_menu("Add Nodes") - - # Insert each category as a submenu under "Add Nodes" - for category, node_classes in custom_nodes_by_category.items(): - category_menu = add_nodes_menu.add_menu(category) - for node_class in node_classes: - node_type = f"{node_class.__identifier__}.{node_class.__name__}" - node_name = node_class.NODE_NAME - category_menu.add_command( - node_name, - make_node_command(graph, node_type) - ) - - # Provide a way to remove selected nodes from the root menu (not in Add Nodes) - graph_context_menu.add_command( - "Remove Selected Node", - lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None - ) - - # Add a top-level "Workflows" menu for saving/loading/closing - workflow_menu = graph_context_menu.add_menu("Workflows") - workflow_menu.add_command("Load Workflow", lambda: load_workflow(graph)) - workflow_menu.add_command("Save Workflow", lambda: save_workflow(graph)) - workflow_menu.add_command("Close Workflow", lambda: close_workflow(graph)) - - # ------------------------------------------------------------------ - # Custom stylesheet to control highlight color & overall QMenu look: - # ------------------------------------------------------------------ - menu_stylesheet = """ - QMenu { - background-color: rgb(30, 30, 30); - border: 1px solid rgba(200, 200, 200, 60); - } - QMenu::item { - padding: 5px 18px 2px; - background-color: transparent; - } - QMenu::item:selected { - color: rgb(255, 255, 255); - background-color: rgb(60, 120, 180); /* set your highlight color here */ - } - QMenu::separator { - height: 1px; - background: rgba(255,255,255,20); - margin: 4px 8px; - } - """ - # Apply to each top-level menu. Submenus inherit from the parent menu. - graph_context_menu.qmenu.setStyleSheet(menu_stylesheet) - add_nodes_menu.qmenu.setStyleSheet(menu_stylesheet) - workflow_menu.qmenu.setStyleSheet(menu_stylesheet) - # ------------------------------------------------------------------ - - # Grid styling changes - graph.set_background_color(20, 20, 20) # Dark gray - graph.set_grid_color(60, 60, 60) # Gray grid lines - - # Add gradient background - scene = graph.scene() - gradient = QtGui.QLinearGradient(0, 0, 0, 1) - gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode) - gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68)) - gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30)) - gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30)) - gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68)) - scene.setBackgroundBrush(QtGui.QBrush(gradient)) - - # Resize and show the graph widget - graph.widget.resize(1600, 900) - graph.widget.show() - - # Global update function - def global_update(): - for node in graph.all_nodes(): - if hasattr(node, "process_input"): - try: - node.process_input() - except Exception as e: - print("Error updating node", node, e) - - timer = QtCore.QTimer() - timer.timeout.connect(global_update) - timer.start(500) - - sys.exit(app.exec_()) + main()