Added Status bar, menu bar, and made context menu blue.
This commit is contained in:
parent
038de06bfd
commit
17f3be4c47
98
Experiments/gui_elements.py
Normal file
98
Experiments/gui_elements.py
Normal 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()
|
399
borealis.py
399
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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user