Added loading window and window group switching
This commit is contained in:
BIN
assets/images/logo@300.png
Normal file
BIN
assets/images/logo@300.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -8,6 +8,9 @@ dependencies = [
|
|||||||
"dearpygui>=2.3.1",
|
"dearpygui>=2.3.1",
|
||||||
"dpg-gauges",
|
"dpg-gauges",
|
||||||
"dynalab-protocol",
|
"dynalab-protocol",
|
||||||
|
"pathlib>=1.0.1",
|
||||||
|
"platformdirs>=4.10.0",
|
||||||
|
"tomli-w>=1.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -11,11 +11,19 @@ import traceback
|
|||||||
import dearpygui.dearpygui as dpg
|
import dearpygui.dearpygui as dpg
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from dynalab.config import ConfigLoadError, load_config
|
||||||
from dynalab.data.json import json_thread_main
|
from dynalab.data.json import json_thread_main
|
||||||
from dynalab.state import AppState
|
from dynalab.state import AppState
|
||||||
from dynalab.tags import MENU_WINDOW_DATA_INPUT
|
from dynalab.tags import WINDOW_DATA_INPUT_GAUGE_GRID
|
||||||
|
from dynalab.ui.error import build_error_modal, show_error_modal
|
||||||
import dynalab.ui.windows
|
import dynalab.ui.windows
|
||||||
from dynalab.ui.windows import build_loading, build_windows
|
from dynalab.ui.windows import (
|
||||||
|
LOADING_WINDOW_HEIGHT,
|
||||||
|
LOADING_WINDOW_WIDTH,
|
||||||
|
build_loading,
|
||||||
|
build_windows,
|
||||||
|
center_loading_content,
|
||||||
|
)
|
||||||
|
|
||||||
import dpg_gauges as dpgg
|
import dpg_gauges as dpgg
|
||||||
from dpg_gauges.exceptions import GaugeNotFoundError
|
from dpg_gauges.exceptions import GaugeNotFoundError
|
||||||
@@ -56,24 +64,52 @@ def _asset_path(relative_path: str) -> str:
|
|||||||
def _init_worker(
|
def _init_worker(
|
||||||
state: AppState, stop_event: threading.Event, message_queue: Queue[str]
|
state: AppState, stop_event: threading.Event, message_queue: Queue[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
time.sleep(1.0)
|
try:
|
||||||
|
time.sleep(1.0)
|
||||||
|
message_queue.put("Fetching DynaLab config")
|
||||||
|
time.sleep(0.5)
|
||||||
|
state.app_config = load_config()
|
||||||
|
message_queue.put("Fetched DynaLab config")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
message_queue.put("Starting JSON data server")
|
message_queue.put("Starting JSON data server")
|
||||||
# Init and start json connector thread
|
# Init and start json connector thread
|
||||||
state.data_json_thread = Thread(
|
state.data_json_thread = Thread(
|
||||||
target=json_thread_main,
|
target=json_thread_main,
|
||||||
name="json-connector-network",
|
name="json-connector-network",
|
||||||
daemon=True,
|
daemon=True,
|
||||||
args=(state,),
|
args=(state,),
|
||||||
)
|
)
|
||||||
state.data_json_thread.start()
|
state.data_json_thread.start()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
message_queue.put("JSON data server started")
|
message_queue.put("JSON data server started")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
message_queue.put("DynaLab Ready")
|
message_queue.put("DynaLab Ready")
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
|
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
except ConfigLoadError as exc:
|
||||||
|
state.init_error = str(exc)
|
||||||
|
state.stop_init.set()
|
||||||
|
except Exception as exc:
|
||||||
|
state.init_error = f"Unexpected startup error: {exc}"
|
||||||
|
state.stop_init.set()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_image(path: Path, tag: str) -> None:
|
||||||
|
width, height, channels, data = dpg.load_image(f"{path}")
|
||||||
|
with dpg.texture_registry():
|
||||||
|
dpg.add_static_texture(width=width, height=height, default_value=data, tag=tag)
|
||||||
|
|
||||||
|
|
||||||
|
def _maximize_viewport() -> None:
|
||||||
|
maximize_viewport = getattr(dpg, "maximize_viewport", None)
|
||||||
|
if callable(maximize_viewport):
|
||||||
|
maximize_viewport()
|
||||||
|
return
|
||||||
|
|
||||||
|
dpg.set_viewport_height(720)
|
||||||
|
dpg.set_viewport_width(1280)
|
||||||
|
|
||||||
|
|
||||||
def run() -> None:
|
def run() -> None:
|
||||||
@@ -87,7 +123,9 @@ def run() -> None:
|
|||||||
# Switch callback management to manual
|
# Switch callback management to manual
|
||||||
dpg.configure_app(manual_callback_management=True)
|
dpg.configure_app(manual_callback_management=True)
|
||||||
# Initialize viewport
|
# Initialize viewport
|
||||||
dpg.create_viewport(title="DynaLab", width=600, height=400)
|
dpg.create_viewport(
|
||||||
|
title="DynaLab", width=LOADING_WINDOW_WIDTH, height=LOADING_WINDOW_HEIGHT
|
||||||
|
)
|
||||||
|
|
||||||
# Load fonts at 2x size for HiDPI scaling
|
# Load fonts at 2x size for HiDPI scaling
|
||||||
with dpg.font_registry():
|
with dpg.font_registry():
|
||||||
@@ -101,17 +139,21 @@ def run() -> None:
|
|||||||
)
|
)
|
||||||
dpg.bind_font(app_font)
|
dpg.bind_font(app_font)
|
||||||
|
|
||||||
|
_load_image(_asset_path("assets/images/logo@300.png"), "logo")
|
||||||
|
|
||||||
build_loading()
|
build_loading()
|
||||||
|
build_error_modal()
|
||||||
|
|
||||||
# Open viewport
|
# Open viewport
|
||||||
dpg.setup_dearpygui()
|
dpg.setup_dearpygui()
|
||||||
dpg.show_viewport()
|
dpg.show_viewport()
|
||||||
|
|
||||||
# Config main window size
|
dpg.configure_item(
|
||||||
vp_w = dpg.get_viewport_client_width()
|
"loading_window",
|
||||||
vp_h = dpg.get_viewport_client_height()
|
pos=(0, 0),
|
||||||
dpg.configure_item("loading_window", pos=(0, 0), width=vp_w, height=vp_h)
|
width=LOADING_WINDOW_WIDTH,
|
||||||
dpg.set_primary_window("loading_window", True)
|
height=LOADING_WINDOW_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
# Init global app state
|
# Init global app state
|
||||||
state: AppState = AppState()
|
state: AppState = AppState()
|
||||||
@@ -128,12 +170,17 @@ def run() -> None:
|
|||||||
|
|
||||||
while not loading_stop.is_set():
|
while not loading_stop.is_set():
|
||||||
# Fetch and execute callbacks
|
# Fetch and execute callbacks
|
||||||
|
if state.stop_init.is_set():
|
||||||
|
show_error_modal(
|
||||||
|
state.init_error if state.init_error is not None else "Fatal error"
|
||||||
|
)
|
||||||
jobs = dpg.get_callback_queue()
|
jobs = dpg.get_callback_queue()
|
||||||
try:
|
try:
|
||||||
dpg.run_callbacks(jobs)
|
dpg.run_callbacks(jobs)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
center_loading_content()
|
||||||
try:
|
try:
|
||||||
msg = loading_msg_queue.get_nowait()
|
msg = loading_msg_queue.get_nowait()
|
||||||
except Empty:
|
except Empty:
|
||||||
@@ -145,9 +192,8 @@ def run() -> None:
|
|||||||
|
|
||||||
dpg.delete_item("loading_window")
|
dpg.delete_item("loading_window")
|
||||||
# Call ui building function
|
# Call ui building function
|
||||||
dpg.set_viewport_height(720)
|
_maximize_viewport()
|
||||||
dpg.set_viewport_width(1280)
|
build_windows(state)
|
||||||
build_windows()
|
|
||||||
|
|
||||||
# Config main window size
|
# Config main window size
|
||||||
vp_w = dpg.get_viewport_client_width()
|
vp_w = dpg.get_viewport_client_width()
|
||||||
@@ -172,7 +218,7 @@ def run() -> None:
|
|||||||
dpgg.digital_gauge(
|
dpgg.digital_gauge(
|
||||||
tag=f"{new_signal.id}",
|
tag=f"{new_signal.id}",
|
||||||
label=f"{new_signal.name}",
|
label=f"{new_signal.name}",
|
||||||
parent=MENU_WINDOW_DATA_INPUT,
|
parent=WINDOW_DATA_INPUT_GAUGE_GRID,
|
||||||
unit=new_signal.unit if new_signal.unit is not None else "",
|
unit=new_signal.unit if new_signal.unit is not None else "",
|
||||||
value=0,
|
value=0,
|
||||||
precision=1,
|
precision=1,
|
||||||
|
|||||||
132
src/dynalab/config.py
Normal file
132
src/dynalab/config.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
import tomli_w
|
||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
|
APP_NAME = "DynaLab"
|
||||||
|
APP_AUTHOR = "Exergie"
|
||||||
|
|
||||||
|
|
||||||
|
class JsonConnectorConfig(BaseModel):
|
||||||
|
host: str = "127.0.0.1"
|
||||||
|
port: int = Field(default=8765, ge=1, le=65535)
|
||||||
|
|
||||||
|
|
||||||
|
class AppConfig(BaseModel):
|
||||||
|
json_connector: JsonConnectorConfig = JsonConnectorConfig()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigLoadError(Exception):
|
||||||
|
path: Path
|
||||||
|
message: str
|
||||||
|
line: int | None
|
||||||
|
column: int | None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path: Path,
|
||||||
|
message: str,
|
||||||
|
line: int | None = None,
|
||||||
|
column: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.path = path
|
||||||
|
self.message = message
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
super().__init__(path, message, line, column)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.line is not None:
|
||||||
|
return f"{self.path}: line {self.line}: {self.message}"
|
||||||
|
|
||||||
|
return f"{self.path}: {self.message}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_path() -> Path:
|
||||||
|
config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR))
|
||||||
|
return config_dir / "config.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def find_toml_line(text: str, field_path: tuple[str, ...]) -> int | None:
|
||||||
|
if not field_path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
section = ".".join(field_path[:-1])
|
||||||
|
key = field_path[-1]
|
||||||
|
current_section = ""
|
||||||
|
|
||||||
|
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if stripped.startswith("[") and stripped.endswith("]"):
|
||||||
|
current_section = stripped.strip("[]").strip()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_section != section or "=" not in stripped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
line_key = stripped.split("=", 1)[0].strip()
|
||||||
|
if line_key == key:
|
||||||
|
return line_number
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> AppConfig:
|
||||||
|
path = get_config_path()
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
config = AppConfig()
|
||||||
|
save_config(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
raise ConfigLoadError(path, f"Failed to read config: {exc}") from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = tomllib.loads(text)
|
||||||
|
except tomllib.TOMLDecodeError as exc:
|
||||||
|
line = getattr(exc, "lineno", None)
|
||||||
|
column = getattr(exc, "colno", None)
|
||||||
|
message = getattr(exc, "msg", str(exc))
|
||||||
|
|
||||||
|
if line is None or column is None:
|
||||||
|
suffix = " (at line "
|
||||||
|
raw_message = str(exc)
|
||||||
|
location_start = raw_message.rfind(suffix)
|
||||||
|
if location_start != -1 and raw_message.endswith(")"):
|
||||||
|
location = raw_message[location_start + len(suffix) : -1]
|
||||||
|
parts = location.split(", column ", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
try:
|
||||||
|
line = int(parts[0])
|
||||||
|
column = int(parts[1])
|
||||||
|
message = raw_message[:location_start]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ConfigLoadError(path, message, line, column) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
return AppConfig.model_validate(data)
|
||||||
|
except ValidationError as exc:
|
||||||
|
error = exc.errors()[0]
|
||||||
|
field_path = tuple(str(part) for part in error["loc"])
|
||||||
|
field_name = ".".join(field_path)
|
||||||
|
line = find_toml_line(text, field_path)
|
||||||
|
message = f"Invalid value for '{field_name}': {error['msg']}"
|
||||||
|
raise ConfigLoadError(path, message, line) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config: AppConfig) -> None:
|
||||||
|
path = get_config_path()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with path.open("wb") as f:
|
||||||
|
tomli_w.dump(config.model_dump(), f)
|
||||||
@@ -121,10 +121,14 @@ async def _handle_connector(
|
|||||||
|
|
||||||
async def _json_main(state: AppState) -> None:
|
async def _json_main(state: AppState) -> None:
|
||||||
server = await asyncio.start_server(
|
server = await asyncio.start_server(
|
||||||
partial(_handle_connector, state), host="127.0.0.1", port=8765
|
partial(_handle_connector, state),
|
||||||
|
host=state.app_config.json_connector.host,
|
||||||
|
port=state.app_config.json_connector.port,
|
||||||
)
|
)
|
||||||
|
|
||||||
print("Json connector server listening on 127.0.0.1:8765")
|
print(
|
||||||
|
f"Json connector server listening on {state.app_config.json_connector.host}:{state.app_config.json_connector.port}"
|
||||||
|
)
|
||||||
|
|
||||||
async with server:
|
async with server:
|
||||||
while not state.stop_event.is_set():
|
while not state.stop_event.is_set():
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from uuid import UUID
|
|||||||
|
|
||||||
from dynalab_protocol.models import SignalDescriptor
|
from dynalab_protocol.models import SignalDescriptor
|
||||||
|
|
||||||
|
from dynalab.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AppState:
|
class AppState:
|
||||||
@@ -19,3 +21,10 @@ class AppState:
|
|||||||
|
|
||||||
live_data: dict[UUID, float] = field(default_factory=dict)
|
live_data: dict[UUID, float] = field(default_factory=dict)
|
||||||
new_data_id: Queue[UUID] = Queue()
|
new_data_id: Queue[UUID] = Queue()
|
||||||
|
|
||||||
|
app_config: AppConfig = field(default_factory=AppConfig)
|
||||||
|
|
||||||
|
stop_init = threading.Event()
|
||||||
|
init_error: str | None = None
|
||||||
|
|
||||||
|
window_map: dict[str, str] = field(default_factory=dict)
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Window groups
|
||||||
|
WINDOW_GROUP_DATA_INPUT: str = "window_group_data_input"
|
||||||
|
WINDOW_GROUP_RECORD: str = "window_group_record"
|
||||||
|
|
||||||
|
WINDOW_DATA_INPUT_GAUGE_GRID: str = "window_data_input_gauge_grid"
|
||||||
|
|
||||||
|
# Menu buttons
|
||||||
|
MENU_FILE_QUIT: str = "menu_file_quit"
|
||||||
|
MENU_RECORD_CONFIGURATION: str = "menu_record_configuration"
|
||||||
|
MENU_WINDOW_RECORD: str = "menu_window_record"
|
||||||
MENU_WINDOW_DATA_INPUT: str = "menu_window_data_input"
|
MENU_WINDOW_DATA_INPUT: str = "menu_window_data_input"
|
||||||
|
|||||||
80
src/dynalab/ui/error.py
Normal file
80
src/dynalab/ui/error.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_MODAL = "error_modal"
|
||||||
|
ERROR_TEXT = "error_modal_text"
|
||||||
|
ERROR_BUTTON_ROW = "error_modal_button_row"
|
||||||
|
ERROR_BUTTON_LEFT_SPACER = "error_modal_button_left_spacer"
|
||||||
|
ERROR_OK_BUTTON = "error_modal_ok_button"
|
||||||
|
ERROR_QUIT_BUTTON = "error_modal_quit_button"
|
||||||
|
ERROR_MODAL_WIDTH = 420
|
||||||
|
ERROR_TEXT_WRAP = 380
|
||||||
|
ERROR_BUTTON_WIDTH = 100
|
||||||
|
ERROR_BUTTON_SPACING = 8
|
||||||
|
|
||||||
|
|
||||||
|
def build_error_modal() -> None:
|
||||||
|
with dpg.window(
|
||||||
|
label="Error",
|
||||||
|
tag=ERROR_MODAL,
|
||||||
|
modal=True,
|
||||||
|
show=False,
|
||||||
|
no_resize=True,
|
||||||
|
no_collapse=True,
|
||||||
|
no_close=True,
|
||||||
|
width=ERROR_MODAL_WIDTH,
|
||||||
|
):
|
||||||
|
dpg.add_text("", tag=ERROR_TEXT, wrap=ERROR_TEXT_WRAP)
|
||||||
|
|
||||||
|
dpg.add_spacer(height=10)
|
||||||
|
|
||||||
|
with dpg.group(horizontal=True, tag=ERROR_BUTTON_ROW):
|
||||||
|
dpg.add_spacer(width=0, tag=ERROR_BUTTON_LEFT_SPACER)
|
||||||
|
dpg.add_button(
|
||||||
|
label="OK",
|
||||||
|
tag=ERROR_OK_BUTTON,
|
||||||
|
width=ERROR_BUTTON_WIDTH,
|
||||||
|
callback=lambda: dpg.hide_item(ERROR_MODAL),
|
||||||
|
)
|
||||||
|
dpg.add_button(
|
||||||
|
label="Quit",
|
||||||
|
tag=ERROR_QUIT_BUTTON,
|
||||||
|
width=ERROR_BUTTON_WIDTH,
|
||||||
|
callback=lambda: exit(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _center_error_buttons() -> None:
|
||||||
|
if not dpg.does_item_exist(ERROR_BUTTON_LEFT_SPACER):
|
||||||
|
return
|
||||||
|
|
||||||
|
modal_width = dpg.get_item_width(ERROR_MODAL)
|
||||||
|
content_width = (
|
||||||
|
min(modal_width, ERROR_TEXT_WRAP) if modal_width > 0 else ERROR_TEXT_WRAP
|
||||||
|
)
|
||||||
|
buttons_width = (ERROR_BUTTON_WIDTH * 2) + ERROR_BUTTON_SPACING
|
||||||
|
spacer_width = max(
|
||||||
|
(content_width - buttons_width) / 2 - ERROR_BUTTON_SPACING,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
dpg.set_item_width(ERROR_BUTTON_LEFT_SPACER, spacer_width)
|
||||||
|
|
||||||
|
|
||||||
|
def show_error_modal(message: str) -> None:
|
||||||
|
dpg.set_value(ERROR_TEXT, message)
|
||||||
|
dpg.show_item(ERROR_MODAL)
|
||||||
|
|
||||||
|
# Optional: center it after showing
|
||||||
|
viewport_width = dpg.get_viewport_client_width()
|
||||||
|
viewport_height = dpg.get_viewport_client_height()
|
||||||
|
window_width = dpg.get_item_width(ERROR_MODAL)
|
||||||
|
window_height = dpg.get_item_height(ERROR_MODAL)
|
||||||
|
|
||||||
|
dpg.set_item_pos(
|
||||||
|
ERROR_MODAL,
|
||||||
|
[
|
||||||
|
(viewport_width - window_width) // 2,
|
||||||
|
(viewport_height - window_height) // 2,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
_center_error_buttons()
|
||||||
11
src/dynalab/ui/routines.py
Normal file
11
src/dynalab/ui/routines.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from dynalab.state import AppState
|
||||||
|
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
|
||||||
|
def switch_window(sender, app_data, user_data: AppState) -> None:
|
||||||
|
for button_tag, window_group_tag in user_data.window_map.items():
|
||||||
|
if button_tag == sender:
|
||||||
|
dpg.show_item(window_group_tag)
|
||||||
|
else:
|
||||||
|
dpg.hide_item(window_group_tag)
|
||||||
@@ -1,36 +1,136 @@
|
|||||||
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from os import wait
|
|
||||||
import dearpygui.dearpygui as dpg
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
import dpg_gauges as dpgg
|
import dpg_gauges as dpgg
|
||||||
|
|
||||||
from dynalab.tags import MENU_WINDOW_DATA_INPUT
|
from dynalab.state import AppState
|
||||||
|
from dynalab.tags import (
|
||||||
|
MENU_FILE_QUIT,
|
||||||
|
MENU_RECORD_CONFIGURATION,
|
||||||
|
MENU_WINDOW_DATA_INPUT,
|
||||||
|
MENU_WINDOW_RECORD,
|
||||||
|
WINDOW_DATA_INPUT_GAUGE_GRID,
|
||||||
|
WINDOW_GROUP_DATA_INPUT,
|
||||||
|
WINDOW_GROUP_RECORD,
|
||||||
|
)
|
||||||
|
from dynalab.ui.routines import switch_window
|
||||||
|
|
||||||
|
|
||||||
|
LOADING_WINDOW_WIDTH = 600
|
||||||
|
LOADING_WINDOW_HEIGHT = 400
|
||||||
|
|
||||||
|
|
||||||
|
def _loading_content_layout(
|
||||||
|
viewport_size: tuple[int, int],
|
||||||
|
logo_size: tuple[int, int],
|
||||||
|
text_size: tuple[int, int],
|
||||||
|
bottom_margin: int = 24,
|
||||||
|
) -> tuple[list[float], list[float], float]:
|
||||||
|
viewport_width, viewport_height = viewport_size
|
||||||
|
logo_width, logo_height = logo_size
|
||||||
|
text_width, text_height = text_size
|
||||||
|
|
||||||
|
logo_pos = [
|
||||||
|
(viewport_width - logo_width) / 2,
|
||||||
|
(viewport_height - logo_height) / 2,
|
||||||
|
]
|
||||||
|
text_row_pos = [
|
||||||
|
0,
|
||||||
|
max(viewport_height - text_height - bottom_margin, 0),
|
||||||
|
]
|
||||||
|
text_spacer_width = max((viewport_width - text_width) / 2, 0)
|
||||||
|
|
||||||
|
return logo_pos, text_row_pos, text_spacer_width
|
||||||
|
|
||||||
|
|
||||||
def build_loading() -> None:
|
def build_loading() -> None:
|
||||||
with dpg.window(label="DynaLab", tag="loading_window", no_collapse=True):
|
with dpg.window(
|
||||||
|
label="DynaLab",
|
||||||
|
tag="loading_window",
|
||||||
|
width=LOADING_WINDOW_WIDTH,
|
||||||
|
height=LOADING_WINDOW_HEIGHT,
|
||||||
|
no_collapse=True,
|
||||||
|
no_resize=True,
|
||||||
|
no_move=True,
|
||||||
|
no_title_bar=True,
|
||||||
|
):
|
||||||
dpg.set_global_font_scale(0.5)
|
dpg.set_global_font_scale(0.5)
|
||||||
dpg.add_text(default_value="Loading DynaLab", tag="loading_text")
|
|
||||||
|
with dpg.group(horizontal=False, tag="loading_logo_group"):
|
||||||
|
dpg.add_image(texture_tag="logo", tag="loading_logo")
|
||||||
|
|
||||||
|
with dpg.group(horizontal=True, tag="loading_text_row"):
|
||||||
|
dpg.add_spacer(width=0, tag="loading_text_left_spacer")
|
||||||
|
dpg.add_text(
|
||||||
|
default_value="Loading DynaLab",
|
||||||
|
tag="loading_text",
|
||||||
|
color=(150, 150, 150, 255),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_windows() -> None:
|
def center_loading_content() -> None:
|
||||||
|
if not dpg.does_item_exist("loading_window"):
|
||||||
|
return
|
||||||
|
|
||||||
|
dpg.set_item_pos("loading_window", [0, 0])
|
||||||
|
dpg.set_item_width("loading_window", LOADING_WINDOW_WIDTH)
|
||||||
|
dpg.set_item_height("loading_window", LOADING_WINDOW_HEIGHT)
|
||||||
|
|
||||||
|
# These may be unavailable before the first rendered frame
|
||||||
|
logo_size = dpg.get_item_rect_size("loading_logo")
|
||||||
|
text_size = dpg.get_item_rect_size("loading_text")
|
||||||
|
|
||||||
|
if logo_size is None or text_size is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
logo_pos, text_row_pos, text_spacer_width = _loading_content_layout(
|
||||||
|
(LOADING_WINDOW_WIDTH, LOADING_WINDOW_HEIGHT), logo_size, text_size
|
||||||
|
)
|
||||||
|
|
||||||
|
dpg.set_item_pos("loading_logo_group", logo_pos)
|
||||||
|
dpg.set_item_pos("loading_text_row", text_row_pos)
|
||||||
|
dpg.set_item_width("loading_text_left_spacer", text_spacer_width)
|
||||||
|
|
||||||
|
|
||||||
|
def build_windows(state: AppState) -> None:
|
||||||
with dpg.window(label="DynaLab", tag="main_window", no_collapse=True):
|
with dpg.window(label="DynaLab", tag="main_window", no_collapse=True):
|
||||||
with dpg.menu_bar():
|
with dpg.menu_bar():
|
||||||
with dpg.menu(label="File"):
|
with dpg.menu(label="File"):
|
||||||
dpg.add_menu_item(label="Quit", enabled=True)
|
dpg.add_menu_item(tag=MENU_FILE_QUIT, label="Quit", enabled=True)
|
||||||
|
with dpg.menu(label="Record"):
|
||||||
|
dpg.add_menu_item(tag=MENU_RECORD_CONFIGURATION, label="Configuration")
|
||||||
with dpg.menu(label="Window"):
|
with dpg.menu(label="Window"):
|
||||||
dpg.add_menu_item(label="Data Input")
|
dpg.add_menu_item(
|
||||||
|
tag=MENU_WINDOW_RECORD,
|
||||||
|
label="Record",
|
||||||
|
callback=switch_window,
|
||||||
|
user_data=state,
|
||||||
|
)
|
||||||
|
dpg.add_menu_item(
|
||||||
|
tag=MENU_WINDOW_DATA_INPUT,
|
||||||
|
label="Data Input",
|
||||||
|
callback=switch_window,
|
||||||
|
user_data=state,
|
||||||
|
)
|
||||||
|
|
||||||
with dpg.child_window(
|
with dpg.child_window(
|
||||||
tag="content_area", autosize_x=True, height=0, border=False
|
tag="content_area", autosize_x=True, height=0, border=False
|
||||||
):
|
):
|
||||||
# with dpg.group(tag=MENU_WINDOW_DATA_INPUT):
|
# with dpg.group(tag=MENU_WINDOW_DATA_INPUT):
|
||||||
with dpgg.gauge_panel(
|
with dpg.group(tag=WINDOW_GROUP_DATA_INPUT):
|
||||||
label="Autobreak narrow panel", autosize_x=True, autosize_y=True
|
state.window_map[MENU_WINDOW_DATA_INPUT] = WINDOW_GROUP_DATA_INPUT
|
||||||
):
|
with dpgg.gauge_panel(
|
||||||
with dpgg.gauge_grid(
|
label="Autobreak narrow panel", autosize_x=True, autosize_y=True
|
||||||
columns="auto", min_column_width=180, tag=MENU_WINDOW_DATA_INPUT
|
|
||||||
):
|
):
|
||||||
pass
|
with dpgg.gauge_grid(
|
||||||
|
columns="auto",
|
||||||
|
min_column_width=180,
|
||||||
|
tag=WINDOW_DATA_INPUT_GAUGE_GRID,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with dpg.group(tag=WINDOW_GROUP_RECORD, show=False):
|
||||||
|
state.window_map[MENU_WINDOW_RECORD] = WINDOW_GROUP_RECORD
|
||||||
|
dpg.add_text("Some text")
|
||||||
|
|||||||
33
uv.lock
generated
33
uv.lock
generated
@@ -42,6 +42,9 @@ dependencies = [
|
|||||||
{ name = "dearpygui" },
|
{ name = "dearpygui" },
|
||||||
{ name = "dpg-gauges" },
|
{ name = "dpg-gauges" },
|
||||||
{ name = "dynalab-protocol" },
|
{ name = "dynalab-protocol" },
|
||||||
|
{ name = "pathlib" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "tomli-w" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -49,6 +52,9 @@ requires-dist = [
|
|||||||
{ name = "dearpygui", specifier = ">=2.3.1" },
|
{ name = "dearpygui", specifier = ">=2.3.1" },
|
||||||
{ name = "dpg-gauges", git = "https://git.h3cx.dev/h3cx/dpg-gauges.git" },
|
{ name = "dpg-gauges", git = "https://git.h3cx.dev/h3cx/dpg-gauges.git" },
|
||||||
{ name = "dynalab-protocol", editable = "../dynalab-protocol" },
|
{ name = "dynalab-protocol", editable = "../dynalab-protocol" },
|
||||||
|
{ name = "pathlib", specifier = ">=1.0.1" },
|
||||||
|
{ name = "platformdirs", specifier = ">=4.10.0" },
|
||||||
|
{ name = "tomli-w", specifier = ">=1.2.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -62,6 +68,24 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "pydantic", specifier = ">=2.13.4" }]
|
requires-dist = [{ name = "pydantic", specifier = ">=2.13.4" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathlib"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298, upload-time = "2014-09-03T15:41:57.18Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363, upload-time = "2022-05-04T13:37:20.585Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.13.4"
|
version = "2.13.4"
|
||||||
@@ -133,6 +157,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli-w"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.16.0"
|
version = "4.16.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user