Files
NegStation/ui.py
2025-07-28 16:03:34 +02:00

128 lines
5.4 KiB
Python

import dearpygui.dearpygui as dpg
import os
import json
import logging
import importlib
import inspect
from collections import deque
import global_state
from widgets.base_widget import BaseWidget
class DpgLogHandler(logging.Handler):
def __init__(self):
super().__init__()
self.log_queue = deque(maxlen=200)
def emit(self, record):
msg = self.format(record)
self.log_queue.append(msg)
print(msg)
def get_all_logs(self):
return "\n".join(self.log_queue)
log_handler = DpgLogHandler()
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[log_handler])
INI_PATH = "negstation_layout.ini"
WIDGET_DATA_PATH = "negstation_widgets.json"
class LayoutManager:
def __init__(self):
self.active_widgets = {}
self.widget_classes = {}
self.updating_widgets = []
self.global_state = global_state.GlobalState()
def discover_and_register_widgets(self, directory="widgets"):
"""Dynamically discovers and registers widgets from a given directory."""
logging.info(f"Discovering widgets in '{directory}' directory...")
for filename in os.listdir(directory):
if filename.endswith(".py") and not filename.startswith("__"):
module_name = f"{directory}.{filename[:-3]}"
try:
# Dynamically import the module
module = importlib.import_module(module_name)
# Find all classes in the module that are subclasses of BaseWidget
for name, cls in inspect.getmembers(module, inspect.isclass):
if issubclass(cls, BaseWidget) and cls is not BaseWidget:
logging.info(f" -> Found and registered widget: {name}")
self.register_widget(name, cls)
except ImportError as e:
logging.error(f"Failed to import widget module {module_name}: {e}")
def register_widget(self, name: str, widget_class: object):
"""Adds a widget class to the registry."""
if name in self.widget_classes:
logging.warning(f"Widget '{name}' is already registered. Overwriting.")
self.widget_classes[name] = widget_class
def add_widget(self, widget_type: str):
if widget_type not in self.widget_classes: logging.error(f"Unknown widget type '{widget_type}'"); return
if widget_type in self.active_widgets:
widget_tag = self.active_widgets[widget_type].window_tag
if dpg.does_item_exist(widget_tag):
logging.info(f"Showing existing widget: {widget_type}"); dpg.configure_item(widget_tag, show=True); dpg.focus_item(widget_tag)
return
config = {"label": widget_type}
WidgetClass = self.widget_classes[widget_type]
widget_instance = WidgetClass(widget_type, config, self, self.global_state)
logging.info(f"Creating new widget of type: {widget_type}")
self.active_widgets[widget_type] = widget_instance
widget_instance.create()
def save_layout(self):
logging.info("Saving layout..."); dpg.save_init_file(INI_PATH)
widget_data = [{"widget_type": w_type, "config": w.get_config()} for w_type, w in self.active_widgets.items()]
with open(WIDGET_DATA_PATH, 'w') as f: json.dump(widget_data, f, indent=4)
logging.info("Layout saved successfully.")
def load_layout(self):
logging.info("Loading layout...");
if not os.path.exists(WIDGET_DATA_PATH): return
with open(WIDGET_DATA_PATH, 'r') as f: widget_data = json.load(f)
for data in widget_data:
if data.get("widget_type") in self.widget_classes: self.add_widget(widget_type=data.get("widget_type"))
if os.path.exists(INI_PATH): dpg.configure_app(init_file=INI_PATH); logging.info(f"Applied UI layout from {INI_PATH}")
def update_all_widgets(self):
"""Calls per-frame update methods on widgets that need it."""
if "LogWidget" in self.active_widgets:
# We need to pass the handler to the update method
self.active_widgets["LogWidget"].update_logs(log_handler)
for w in self.updating_widgets:
if w in self.active_widgets:
self.active_widgets[w].update()
@staticmethod
def run():
dpg.create_context()
dpg.create_viewport(title='Dynamic Docking Layout with Menu', width=1280, height=720)
layout_manager = LayoutManager()
layout_manager.discover_and_register_widgets()
with dpg.viewport_menu_bar():
with dpg.menu(label="File"):
dpg.add_menu_item(label="Save Layout", callback=layout_manager.save_layout)
with dpg.menu(label="View"):
for widget_name in sorted(layout_manager.widget_classes.keys()):
dpg.add_menu_item(
label=f"Show {widget_name}",
callback=lambda s, a, ud: layout_manager.add_widget(ud),
user_data=widget_name
)
dpg.configure_app(docking=True, docking_space=True)
dpg.setup_dearpygui()
layout_manager.load_layout()
dpg.show_viewport()
while dpg.is_dearpygui_running():
layout_manager.update_all_widgets()
dpg.render_dearpygui_frame()
dpg.destroy_context()