commit 4e1a9f0182c96e00814785915e7b78545ad46d9e Author: Jojojoppe Date: Mon Jul 28 15:00:25 2025 +0200 Basic GUI infrastructure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96571bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +env \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5f3b837 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +import ui + +if __name__=="__main__": + ui.LayoutManager.run() \ No newline at end of file diff --git a/requirements b/requirements new file mode 100644 index 0000000..ad6ee52 --- /dev/null +++ b/requirements @@ -0,0 +1,5 @@ +pillow +gphoto2 +dearpygui +numpy +rawpy \ No newline at end of file diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..950f417 --- /dev/null +++ b/ui.py @@ -0,0 +1,122 @@ +import dearpygui.dearpygui as dpg +import os +import json +import logging +import importlib +import inspect +from collections import deque + +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 = {} + + 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) + 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) + + @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() \ No newline at end of file diff --git a/widgets/__init__.py b/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/widgets/base_widget.py b/widgets/base_widget.py new file mode 100644 index 0000000..35ced31 --- /dev/null +++ b/widgets/base_widget.py @@ -0,0 +1,20 @@ +import dearpygui.dearpygui as dpg +import logging + +class BaseWidget: + """A base class to handle common functionality for all widgets.""" + def __init__(self, widget_type: str, config: dict, layout_manager): + self.widget_type = widget_type + self.config = config + self.layout_manager = layout_manager + self.window_tag = f"widget_win_{self.widget_type}" + + def create(self): + raise NotImplementedError + + def get_config(self) -> dict: + return self.config + + def _on_window_close(self, sender, app_data, user_data): + logging.info(f"Hiding widget: {self.widget_type}") + dpg.configure_item(self.window_tag, show=False) \ No newline at end of file diff --git a/widgets/log_widget.py b/widgets/log_widget.py new file mode 100644 index 0000000..da6535c --- /dev/null +++ b/widgets/log_widget.py @@ -0,0 +1,17 @@ +import dearpygui.dearpygui as dpg +from .base_widget import BaseWidget + +class LogWidget(BaseWidget): + """A widget to display captured log messages.""" + def create(self): + if dpg.does_item_exist(self.window_tag): return + with dpg.window(label="Log Viewer", tag=self.window_tag, on_close=self._on_window_close, width=500, height=300): + self.text_item_tag = dpg.add_input_text( + multiline=True, readonly=True, width=-1, height=-1, default_value="Log initialized.\n" + ) + + def update_logs(self, log_handler): + """Called every frame to update the text with new logs.""" + if dpg.is_item_visible(self.window_tag): + log_content = log_handler.get_all_logs() + dpg.set_value(self.text_item_tag, log_content) \ No newline at end of file diff --git a/widgets/simple_widget.py b/widgets/simple_widget.py new file mode 100644 index 0000000..81e7d91 --- /dev/null +++ b/widgets/simple_widget.py @@ -0,0 +1,10 @@ +import dearpygui.dearpygui as dpg +from .base_widget import BaseWidget + +class SimpleWidget(BaseWidget): + """A basic text widget to demonstrate dynamic loading.""" + def create(self): + if dpg.does_item_exist(self.window_tag): return + with dpg.window(label="Simple Widget", tag=self.window_tag, on_close=self._on_window_close, width=300, height=120): + dpg.add_text("This widget was loaded dynamically!") + dpg.add_button(label="A Button") \ No newline at end of file