Added Status bar, menu bar, and made context menu blue.

This commit is contained in:
Nicole Rappe 2025-02-25 17:20:31 -07:00
parent 038de06bfd
commit 17f3be4c47
2 changed files with 340 additions and 157 deletions

View File

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

View File

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