web UI added and refactoring done
This commit is contained in:
@@ -3,10 +3,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, QSpinBox
|
||||
|
||||
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
|
||||
|
||||
_GPR_MODES = ("gpr", "legacy_gpr")
|
||||
|
||||
|
||||
def _is_legacy_gpr(window) -> bool:
|
||||
return window._processing_mode.currentText() == "legacy_gpr"
|
||||
|
||||
|
||||
def _attr(name: str):
|
||||
"""Widget getter for a field backed by a single fixed widget."""
|
||||
return lambda window: getattr(window, name)
|
||||
|
||||
|
||||
def _dual(gpr_attr: str, legacy_attr: str):
|
||||
"""Widget getter for a gpr_* field with separate gpr / legacy_gpr widgets."""
|
||||
return lambda window: getattr(window, legacy_attr if _is_legacy_gpr(window) else gpr_attr)
|
||||
|
||||
|
||||
# The ONE authoritative field <-> widget map (inverse of `_live_processing_config`),
|
||||
# plus each field's display group and the processor mode(s) it applies to. Everything
|
||||
# else the web form needs — input type, combobox options, numeric ranges, the current
|
||||
# value, the enabled state — is read live FROM these widgets by `_build_web_settings_schema`,
|
||||
# so the web hardcodes none of it and always mirrors the desktop. `modes=None` => all modes.
|
||||
# Entries: (field, group, modes, widget_getter).
|
||||
_WEB_LIVE_SCHEMA = [
|
||||
("processor_mode", "Processor", None, _attr("_processing_mode")),
|
||||
("pass_through_fixed_y_enabled", "Pass-through", ("pass_through",), _attr("_pass_through_fixed_y_enabled")),
|
||||
("pass_through_y_min_db", "Pass-through", ("pass_through",), _attr("_pass_through_y_min_db")),
|
||||
("pass_through_y_max_db", "Pass-through", ("pass_through",), _attr("_pass_through_y_max_db")),
|
||||
("bscan_axis", "B-scan", ("bscan",), _attr("_bscan_axis")),
|
||||
("bscan_cut_m", "B-scan", ("bscan",), _attr("_bscan_cut_m")),
|
||||
("bscan_max_depth_m", "B-scan", ("bscan",), _attr("_bscan_max_depth_m")),
|
||||
("bscan_gain", "B-scan", ("bscan",), _attr("_bscan_gain")),
|
||||
("bscan_start_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_start_freq_mhz")),
|
||||
("bscan_stop_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_stop_freq_mhz")),
|
||||
("legacy_gpr_mode", "Mode", ("legacy_gpr",), _attr("_legacy_gpr_config_mode")),
|
||||
("gpr_input_positions", "Geometry & depth", _GPR_MODES, _dual("_gpr_input_positions_input", "_legacy_gpr_input_positions_input")),
|
||||
("gpr_output_positions", "Geometry & depth", _GPR_MODES, _dual("_gpr_output_positions_input", "_legacy_gpr_output_positions_input")),
|
||||
("gpr_min_depth_m", "Geometry & depth", _GPR_MODES, _dual("_gpr_min_depth_m", "_legacy_gpr_min_depth_m")),
|
||||
("gpr_max_depth_m", "Geometry & depth", _GPR_MODES, _dual("_gpr_max_depth_m", "_legacy_gpr_max_depth_m")),
|
||||
("gpr_start_freq_mhz", "Geometry & depth", _GPR_MODES, _dual("_gpr_start_freq_mhz", "_legacy_gpr_start_freq_mhz")),
|
||||
("gpr_stop_freq_mhz", "Geometry & depth", _GPR_MODES, _dual("_gpr_stop_freq_mhz", "_legacy_gpr_stop_freq_mhz")),
|
||||
("gpr_imaging_plane_y_m", "Geometry & depth", ("gpr",), _attr("_gpr_imaging_plane_y_m")),
|
||||
("gpr_range_comp_power", "Imaging", ("gpr",), _attr("_gpr_range_comp_power")),
|
||||
("gpr_angle_comp_power", "Imaging", ("gpr",), _attr("_gpr_angle_comp_power")),
|
||||
("gpr_score_mode", "Imaging", ("gpr",), _attr("_gpr_score_mode")),
|
||||
("gpr_background_subtract_enabled", "Imaging", _GPR_MODES, _dual("_gpr_background_subtract_enabled", "_legacy_gpr_background_subtract_enabled")),
|
||||
("gpr_background_mean_count", "Imaging", _GPR_MODES, _dual("_gpr_background_mean_count", "_legacy_gpr_background_mean_count")),
|
||||
("gpr_remove_sidelobe_objects_enabled", "Imaging", ("gpr",), _attr("_gpr_remove_sidelobe_objects_enabled")),
|
||||
("gpr_min_visible_score", "Detection", ("gpr",), _attr("_gpr_min_visible_score")),
|
||||
("gpr_max_detected_objects_to_draw", "Detection", ("gpr",), _attr("_gpr_max_detected_objects_to_draw")),
|
||||
("gpr_draw_top_m_objects", "Detection", ("gpr",), _attr("_gpr_draw_top_m_objects")),
|
||||
("gpr_comp_power", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_comp_power")),
|
||||
("gpr_snr_thresh", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_snr_thresh")),
|
||||
("gpr_snr_comp_max", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_snr_comp_max")),
|
||||
("legacy_gpr_min_visible_pair_count", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_min_visible_pair_count")),
|
||||
("gpr_look_angle_deg", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_look_angle_deg")),
|
||||
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_apply_freq_phase_correction")),
|
||||
("gpr_reference_mode", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_reference_mode")),
|
||||
("ignore_socket_speed", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_ignore_socket_speed_enabled")),
|
||||
("gpr_speed_m_s", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_speed_m_s")),
|
||||
]
|
||||
_WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA}
|
||||
|
||||
|
||||
def _web_field_schema(field: str, group: str, widget) -> dict | None:
|
||||
"""Describe one widget for the web form (type, options/range, value, enabled)."""
|
||||
base = {"name": field, "group": group, "enabled": bool(widget.isEnabled())}
|
||||
if isinstance(widget, QComboBox):
|
||||
return {**base, "kind": "select", "value": widget.currentText(),
|
||||
"options": [widget.itemText(i) for i in range(widget.count())]}
|
||||
if isinstance(widget, QCheckBox):
|
||||
return {**base, "kind": "bool", "value": bool(widget.isChecked())}
|
||||
if isinstance(widget, QSpinBox):
|
||||
return {**base, "kind": "int", "value": int(widget.value()),
|
||||
"min": int(widget.minimum()), "max": int(widget.maximum()), "step": int(widget.singleStep())}
|
||||
if isinstance(widget, QDoubleSpinBox):
|
||||
return {**base, "kind": "float", "value": float(widget.value()),
|
||||
"min": float(widget.minimum()), "max": float(widget.maximum()),
|
||||
"step": float(widget.singleStep()), "decimals": int(widget.decimals())}
|
||||
if isinstance(widget, QLineEdit):
|
||||
return {**base, "kind": "text", "value": widget.text()}
|
||||
return None
|
||||
|
||||
|
||||
def _build_web_settings_schema(window) -> list[dict]:
|
||||
"""Build the settings schema for the active mode straight from the Qt widgets."""
|
||||
mode = window._processing_mode.currentText()
|
||||
schema: list[dict] = []
|
||||
for field, group, modes, getter in _WEB_LIVE_SCHEMA:
|
||||
if modes is not None and mode not in modes:
|
||||
continue
|
||||
entry = _web_field_schema(field, group, getter(window))
|
||||
if entry is not None:
|
||||
schema.append(entry)
|
||||
return schema
|
||||
|
||||
|
||||
def _set_web_live_field(window, field: str, value) -> None:
|
||||
"""Write one web value into the desktop widget that feeds ``field``."""
|
||||
getter = _WEB_LIVE_GETTERS.get(field)
|
||||
if getter is None:
|
||||
return
|
||||
widget = getter(window)
|
||||
if isinstance(widget, QComboBox):
|
||||
window._set_combo_current_text(widget, str(value))
|
||||
elif isinstance(widget, QCheckBox):
|
||||
widget.setChecked(bool(value))
|
||||
elif isinstance(widget, QSpinBox):
|
||||
widget.setValue(int(float(value)))
|
||||
elif isinstance(widget, QDoubleSpinBox):
|
||||
widget.setValue(float(value))
|
||||
elif isinstance(widget, QLineEdit):
|
||||
widget.setText(",".join(str(int(v)) for v in value) if isinstance(value, list) else str(value))
|
||||
|
||||
|
||||
class AppWindowLiveProcessingMixin:
|
||||
"""Handle live processing updates, redraws, and locator republishing."""
|
||||
@@ -55,7 +170,8 @@ class AppWindowLiveProcessingMixin:
|
||||
|
||||
y_min_db = float(self._pass_through_y_min_db.value())
|
||||
y_max_db = float(self._pass_through_y_max_db.value())
|
||||
return ProcessingLiveConfig(
|
||||
visible_x_min, visible_x_max, visible_z_min, visible_z_max = self._gpr_visible_bounds()
|
||||
config = ProcessingLiveConfig(
|
||||
processor_mode=mode,
|
||||
pass_through_channel="s21",
|
||||
pass_through_fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()),
|
||||
@@ -95,11 +211,16 @@ class AppWindowLiveProcessingMixin:
|
||||
gpr_imaging_plane_y_m=float(self._gpr_imaging_plane_y_m.value()),
|
||||
gpr_min_visible_score=float(self._gpr_min_visible_score.value()),
|
||||
legacy_gpr_min_visible_pair_count=float(self._legacy_gpr_min_visible_pair_count.value()),
|
||||
gpr_visible_x_min_m=visible_x_min,
|
||||
gpr_visible_x_max_m=visible_x_max,
|
||||
gpr_visible_z_min_m=visible_z_min,
|
||||
gpr_visible_z_max_m=visible_z_max,
|
||||
ignore_socket_speed=bool(self._legacy_gpr_ignore_socket_speed_enabled.isChecked()),
|
||||
reprocess_current_result=bool(reprocess_current_result),
|
||||
history_command_seq=int(self._history_command_seq),
|
||||
history_command=str(history_command),
|
||||
)
|
||||
return config
|
||||
|
||||
def _write_live_processing_config(
|
||||
self,
|
||||
@@ -118,8 +239,50 @@ class AppWindowLiveProcessingMixin:
|
||||
)
|
||||
)
|
||||
|
||||
def _apply_web_live_settings(self, fields: dict) -> None:
|
||||
"""Apply web-requested live settings through the SAME path as a desktop edit.
|
||||
|
||||
Writes the values into the desktop widgets, then runs the single
|
||||
``_on_processing_live_settings_changed`` handler — one writer, one redraw —
|
||||
so the browser and the desktop never desync and ``processing_live.json`` has
|
||||
exactly one author. The per-widget change signals are neutralized by a
|
||||
suppression flag so the final handler runs once instead of dozens of times;
|
||||
a history command keeps its own server-managed sequence-bump path.
|
||||
"""
|
||||
try:
|
||||
history_command = str(fields.get("history_command", "none"))
|
||||
settings = {
|
||||
name: value
|
||||
for name, value in fields.items()
|
||||
if name not in {"history_command", "history_command_seq"}
|
||||
}
|
||||
self._suppress_live_settings_handler = True
|
||||
try:
|
||||
# processor_mode first: dual-sourced gpr_* fields route to the gpr or
|
||||
# legacy_gpr widget based on the active mode.
|
||||
if "processor_mode" in settings:
|
||||
_set_web_live_field(self, "processor_mode", settings.pop("processor_mode"))
|
||||
for name, value in settings.items():
|
||||
_set_web_live_field(self, name, value)
|
||||
finally:
|
||||
self._suppress_live_settings_handler = False
|
||||
|
||||
if history_command in {"clear_all", "remove_last"}:
|
||||
self._write_live_processing_config(history_command=history_command, bump_history_seq=True)
|
||||
self._on_processing_live_settings_changed()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to apply web live settings", exc)
|
||||
|
||||
def _web_settings_schema(self) -> list[dict]:
|
||||
"""Return the live-settings schema for the active mode (read from widgets)."""
|
||||
return _build_web_settings_schema(self)
|
||||
|
||||
def _on_processing_live_settings_changed(self, *_args) -> None:
|
||||
"""Handle live-processing setting changes and trigger redraw when needed."""
|
||||
# Web-apply sets many widgets at once; their individual change signals are
|
||||
# suppressed so this writer+redraw runs exactly once at the end.
|
||||
if getattr(self, "_suppress_live_settings_handler", False):
|
||||
return
|
||||
try:
|
||||
current_mode = self._processing_mode.currentText()
|
||||
if self._is_gpr_processing_mode(current_mode):
|
||||
|
||||
Reference in New Issue
Block a user