little refactoring done
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
"""UI construction mixin for the main radar control window."""
|
||||
"""UI construction mixin for the main radar control window.
|
||||
|
||||
Layout is intentionally split into two independent plot surfaces:
|
||||
- single `PlotWidget` for B-scan heatmap rendering;
|
||||
- stacked magnitude/phase `PlotWidget`s for pass-through traces.
|
||||
|
||||
`_set_plot_mode()` switches between these surfaces via `QStackedWidget`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,10 +36,21 @@ from python_app.gui.controllers.sections import (
|
||||
|
||||
|
||||
class AppWindowUiMixin:
|
||||
"""Builds and wires all static UI widgets."""
|
||||
"""Build and wire all static widgets used by :class:`AppWindow`.
|
||||
|
||||
This mixin only creates UI objects and connects lightweight UI signals.
|
||||
Runtime logic (pipeline start/stop, polling, drawing, snapshot saving)
|
||||
is implemented in other mixins.
|
||||
"""
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
"""Build main window widgets, plot area, and settings panel."""
|
||||
"""Build complete main-window layout.
|
||||
|
||||
High-level structure:
|
||||
1. Left: plot stack (`B-scan` page + `trace magnitude/phase` page).
|
||||
2. Middle: narrow toggle button to collapse/expand settings.
|
||||
3. Right: scrollable settings panel + status/history + runtime log.
|
||||
"""
|
||||
self.setWindowTitle("Radar System Control")
|
||||
root = QWidget(self)
|
||||
self.setCentralWidget(root)
|
||||
@@ -41,15 +59,37 @@ class AppWindowUiMixin:
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(14)
|
||||
|
||||
self._build_plot_area(layout, root)
|
||||
self._build_settings_toggle(layout)
|
||||
self._build_settings_panel(layout, root)
|
||||
|
||||
self._toggle_settings_panel(visible=True)
|
||||
self.resize(1650, 940)
|
||||
|
||||
def _build_plot_area(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
||||
"""Build left plot area with stacked B-scan and trace pages."""
|
||||
# Plot stack holds mutually exclusive visualization surfaces.
|
||||
# We create both upfront and only switch active page at runtime.
|
||||
self._plot_stack = QStackedWidget(root)
|
||||
self._build_bscan_plot_page()
|
||||
self._build_trace_plot_page()
|
||||
|
||||
self._plot = pg.PlotWidget(background="#0f141c")
|
||||
self._plot.showGrid(x=True, y=True, alpha=0.2)
|
||||
self._plot.setLabel("bottom", "Frequency", units="Hz")
|
||||
self._plot.setLabel("left", "Magnitude", units="dB")
|
||||
self._plot_stack.addWidget(self._plot)
|
||||
# Default view on startup is pass-through traces.
|
||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
||||
root_layout.addWidget(self._plot_stack, stretch=11)
|
||||
|
||||
self._trace_plots_container = QWidget(root)
|
||||
def _build_bscan_plot_page(self) -> None:
|
||||
"""Create B-scan page in plot stack."""
|
||||
# B-scan surface: one PlotWidget used as canvas for ImageItem heatmap.
|
||||
self._bscan_plot = pg.PlotWidget(background="#0f141c")
|
||||
self._bscan_plot.showGrid(x=True, y=True, alpha=0.2)
|
||||
self._plot_stack.addWidget(self._bscan_plot)
|
||||
|
||||
def _build_trace_plot_page(self) -> None:
|
||||
"""Create pass-through page with magnitude and phase plots."""
|
||||
# Pass-through surface: container with two synchronized line plots.
|
||||
# Upper plot shows magnitude, lower plot shows phase.
|
||||
self._trace_plots_container = QWidget()
|
||||
trace_layout = QVBoxLayout(self._trace_plots_container)
|
||||
trace_layout.setContentsMargins(0, 0, 0, 0)
|
||||
trace_layout.setSpacing(6)
|
||||
@@ -70,53 +110,40 @@ class AppWindowUiMixin:
|
||||
self._trace_phase_plot.getPlotItem().setClipToView(True)
|
||||
trace_layout.addWidget(self._trace_phase_plot, stretch=1)
|
||||
|
||||
self._plot_stack.addWidget(self._trace_plots_container)
|
||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
||||
layout.addWidget(self._plot_stack, stretch=11)
|
||||
# Legends are created lazily only when trace mode draws line series.
|
||||
self._trace_magnitude_legend = None
|
||||
self._trace_phase_legend = None
|
||||
self._trace_magnitude_legend_combo_keys = set()
|
||||
self._trace_phase_legend_combo_keys = set()
|
||||
self._trace_magnitude_curves = {}
|
||||
self._trace_phase_curves = {}
|
||||
self._trace_phase_render_max_points = 400
|
||||
|
||||
self._plot_stack.addWidget(self._trace_plots_container)
|
||||
|
||||
def _build_settings_toggle(self, root_layout: QHBoxLayout) -> None:
|
||||
"""Create narrow button used to collapse or show settings panel."""
|
||||
self._settings_toggle_button = QPushButton("<")
|
||||
self._settings_toggle_button.setObjectName("settingsToggleButton")
|
||||
self._settings_toggle_button.setFixedWidth(26)
|
||||
self._settings_toggle_button.clicked.connect(self._toggle_settings_panel)
|
||||
layout.addWidget(self._settings_toggle_button, stretch=0)
|
||||
self._settings_toggle_button.clicked.connect(lambda: self._toggle_settings_panel())
|
||||
root_layout.addWidget(self._settings_toggle_button, stretch=0)
|
||||
|
||||
def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
||||
"""Build right settings panel with controls, status labels, and log."""
|
||||
self._settings_panel = QWidget(root)
|
||||
self._settings_panel.setMinimumWidth(659)
|
||||
right_layout = QVBoxLayout(self._settings_panel)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(10)
|
||||
|
||||
# Build log widget early so error handlers can safely write during UI construction.
|
||||
# Build log early so `_show_error()` can append text even during
|
||||
# subsequent group construction if something fails.
|
||||
self._log_box = QPlainTextEdit(self._settings_panel)
|
||||
self._log_box.setReadOnly(True)
|
||||
self._log_box.setMinimumHeight(170)
|
||||
|
||||
pipeline_group = self._build_pipeline_group()
|
||||
hardware_actions_group = self._build_hardware_actions_group()
|
||||
data_actions_group = self._build_data_actions_group()
|
||||
preprocess_summary_group = self._build_preprocess_summary_group()
|
||||
radar_group = self._build_radar_group()
|
||||
processing_group = self._build_processing_group()
|
||||
switch_group = self._build_switch_group()
|
||||
|
||||
controls = QWidget(self._settings_panel)
|
||||
controls_layout = QVBoxLayout(controls)
|
||||
controls_layout.setContentsMargins(0, 0, 0, 0)
|
||||
controls_layout.setSpacing(10)
|
||||
controls_layout.addWidget(pipeline_group)
|
||||
controls_layout.addWidget(hardware_actions_group)
|
||||
controls_layout.addWidget(data_actions_group)
|
||||
controls_layout.addWidget(preprocess_summary_group)
|
||||
controls_layout.addWidget(processing_group)
|
||||
controls_layout.addWidget(radar_group)
|
||||
controls_layout.addWidget(switch_group)
|
||||
controls_layout.addStretch(1)
|
||||
|
||||
scroll = QScrollArea(self._settings_panel)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setWidget(controls)
|
||||
right_layout.addWidget(scroll, stretch=1)
|
||||
right_layout.addWidget(self._build_settings_scroll(), stretch=1)
|
||||
|
||||
self._status_label = QLabel("Status: idle", self._settings_panel)
|
||||
self._status_label.setObjectName("statusLabel")
|
||||
@@ -127,17 +154,46 @@ class AppWindowUiMixin:
|
||||
right_layout.addWidget(self._history_label)
|
||||
|
||||
right_layout.addWidget(self._log_box, stretch=0)
|
||||
root_layout.addWidget(self._settings_panel, stretch=8)
|
||||
|
||||
layout.addWidget(self._settings_panel, stretch=8)
|
||||
self._set_settings_panel_visible(True)
|
||||
self.resize(1650, 940)
|
||||
def _build_settings_scroll(self) -> QScrollArea:
|
||||
"""Build scroll area with all control groups in display order."""
|
||||
# All control groups are placed into a scroll area so right panel
|
||||
# remains usable on smaller screens and with future extra controls.
|
||||
controls = QWidget(self._settings_panel)
|
||||
controls_layout = QVBoxLayout(controls)
|
||||
controls_layout.setContentsMargins(0, 0, 0, 0)
|
||||
controls_layout.setSpacing(10)
|
||||
for group in self._build_control_groups():
|
||||
controls_layout.addWidget(group)
|
||||
controls_layout.addStretch(1)
|
||||
|
||||
def _toggle_settings_panel(self) -> None:
|
||||
"""Toggle settings panel visibility."""
|
||||
self._set_settings_panel_visible(not self._settings_panel.isVisible())
|
||||
scroll = QScrollArea(self._settings_panel)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setWidget(controls)
|
||||
return scroll
|
||||
|
||||
def _set_settings_panel_visible(self, visible: bool) -> None:
|
||||
"""Set settings panel visibility and update toggle button glyph."""
|
||||
def _build_control_groups(self) -> list[QGroupBox]:
|
||||
"""Create all settings groups in top-to-bottom order."""
|
||||
return [
|
||||
build_pipeline_group(self),
|
||||
build_hardware_actions_group(self),
|
||||
build_data_actions_group(self),
|
||||
build_preprocess_summary_group(self),
|
||||
build_processing_group(self),
|
||||
build_radar_group(self),
|
||||
build_switch_group(self),
|
||||
]
|
||||
|
||||
def _toggle_settings_panel(self, *, visible: bool | None = None) -> None:
|
||||
"""Toggle settings panel visibility or force a specific state.
|
||||
|
||||
When `visible` is `None`, state is toggled.
|
||||
When `visible` is set, panel visibility is forced to that value.
|
||||
"""
|
||||
if visible is None:
|
||||
visible = not self._settings_panel.isVisible()
|
||||
self._settings_panel.setVisible(visible)
|
||||
if visible:
|
||||
self._settings_toggle_button.setText(">")
|
||||
@@ -147,40 +203,16 @@ class AppWindowUiMixin:
|
||||
self._settings_toggle_button.setToolTip("Show settings panel")
|
||||
|
||||
def _set_plot_mode(self, mode: str) -> None:
|
||||
"""Switch visible plot surface based on processing mode."""
|
||||
"""Switch visible plot page according to processing mode.
|
||||
|
||||
`bscan` -> show `self._bscan_plot` (single heatmap surface)
|
||||
otherwise -> show `self._trace_plots_container` (magnitude + phase)
|
||||
"""
|
||||
if mode == "bscan":
|
||||
self._plot_stack.setCurrentWidget(self._plot)
|
||||
self._plot_stack.setCurrentWidget(self._bscan_plot)
|
||||
return
|
||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
||||
|
||||
def _build_pipeline_group(self) -> QGroupBox:
|
||||
"""Build pipeline controls section."""
|
||||
return build_pipeline_group(self)
|
||||
|
||||
def _build_hardware_actions_group(self) -> QGroupBox:
|
||||
"""Build hardware actions section."""
|
||||
return build_hardware_actions_group(self)
|
||||
|
||||
def _build_data_actions_group(self) -> QGroupBox:
|
||||
"""Build data actions section."""
|
||||
return build_data_actions_group(self)
|
||||
|
||||
def _build_preprocess_summary_group(self) -> QGroupBox:
|
||||
"""Build selected preprocess sets summary section."""
|
||||
return build_preprocess_summary_group(self)
|
||||
|
||||
def _build_processing_group(self) -> QGroupBox:
|
||||
"""Build processing mode section."""
|
||||
return build_processing_group(self)
|
||||
|
||||
def _build_radar_group(self) -> QGroupBox:
|
||||
"""Build radar settings section."""
|
||||
return build_radar_group(self)
|
||||
|
||||
def _build_switch_group(self) -> QGroupBox:
|
||||
"""Build switch settings section."""
|
||||
return build_switch_group(self)
|
||||
|
||||
@staticmethod
|
||||
def _set_combo_current_text(combo: QComboBox, value: str) -> None:
|
||||
"""Select combo item by text, appending it when missing."""
|
||||
|
||||
Reference in New Issue
Block a user