Polish GUI design

This commit is contained in:
Ayzen
2026-10-06 14:16:14 +03:00
parent 5bb86b7165
commit 4947d5a14e
25 changed files with 222 additions and 235 deletions
@@ -10,7 +10,7 @@ import math
from pathlib import Path
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QLabel, QSpinBox
from python_app.storage.npz.paths import radar_config_filename_prefix
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
@@ -300,7 +300,18 @@ class ConfigController:
def web_schema(self) -> list[dict[str, object]]:
schema = []
for name, (group, widget) in self._web_widgets(str(self._scalar("_processing_mode"))).items():
entry: dict[str, object] = {"name": name, "group": group, "enabled": widget.isEnabled(), "value": _read_widget(widget)}
if isinstance(widget, QCheckBox):
label = widget.text()
else:
parent = widget.parentWidget()
assert parent is not None
form = parent.layout()
assert isinstance(form, QFormLayout)
caption = form.labelForField(widget)
assert isinstance(caption, QLabel)
label = caption.text()
entry: dict[str, object] = {"name": name, "label": label, "group": group,
"enabled": widget.isEnabled(), "value": _read_widget(widget)}
if isinstance(widget, QComboBox):
entry.update(kind="select", options=[widget.itemText(i) for i in range(widget.count())])
elif isinstance(widget, QCheckBox):
@@ -370,6 +381,5 @@ class ConfigController:
raise
def mode_summary(self, mode: str) -> str:
values = (f"{binding.spec.web}={self._scalar(binding.spec.widget)}"
for binding in active_web_fields(mode) if binding.spec.web != "processor_mode")
return "(" + ", ".join(values) + ")"
return ", ".join(f"{binding.spec.web}={self._scalar(binding.spec.widget)}"
for binding in active_web_fields(mode) if binding.spec.web != "processor_mode")
@@ -200,4 +200,5 @@ class LiveSettingsController:
self.active_mode = mode
self._pipeline.plots.set_mode(mode)
self.set_mode_page(mode)
self._log.info(f"Processing mode selected: {mode} {self.config.mode_summary(mode)}")
self._log.info(f"Processing mode selected: {mode}")
self._log.debug(f"Processing settings ({mode}): {self.config.mode_summary(mode)}")
+8 -6
View File
@@ -12,9 +12,9 @@ from pathlib import Path
import threading
import traceback
from PyQt6.QtCore import QSignalBlocker, QTimer
from PyQt6.QtCore import QSignalBlocker, QTimer, Qt
from PyQt6.QtGui import QTextCursor
from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QTextEdit, QVBoxLayout, QWidget
from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QMessageBox, QToolButton, QTextEdit, QVBoxLayout, QWidget
from python_app.gui.settings_defaults import env_enabled
from python_app.logging_setup import LOG_LEVELS, add_handler, configure_logging, get_logger, package_logger, set_log_level
@@ -102,8 +102,9 @@ class LogController:
self.level.setToolTip("Logging verbosity — applied live and saved to run_config")
self.level.currentTextChanged.connect(self.set_level)
header.addWidget(self.level)
self._toggle = QPushButton("Collapse")
self._toggle.setObjectName("sectionToggleButton")
self._toggle = QToolButton()
self._toggle.setAutoRaise(True)
self._toggle.setArrowType(Qt.ArrowType.DownArrow)
self._toggle.setToolTip("Hide runtime log entries")
self._toggle.clicked.connect(self.toggle)
header.addWidget(self._toggle)
@@ -112,7 +113,8 @@ class LogController:
self._box.setObjectName("runtimeLogBox")
self._box.setReadOnly(True)
self._box.setUndoRedoEnabled(False)
self._box.setMinimumHeight(170)
self._box.setMinimumHeight(130)
self._box.setMaximumHeight(140)
document = self._box.document()
assert document is not None
document.setMaximumBlockCount(1200)
@@ -133,7 +135,7 @@ class LogController:
def toggle(self) -> None:
visible = self._box.isHidden()
self._box.setVisible(visible)
self._toggle.setText("Collapse" if visible else "Expand")
self._toggle.setArrowType(Qt.ArrowType.DownArrow if visible else Qt.ArrowType.RightArrow)
self._toggle.setToolTip("Hide runtime log entries" if visible else "Show runtime log entries")
def flush(self) -> None:
@@ -87,7 +87,7 @@ class RadarSettingsController:
def apply_limits(self, limits: dict[str, float | int] | None) -> bool:
previous, self.limits = self.limits, limits
labels = ("Start Hz", "Stop Hz", "Points", "IF BW Hz", "Stimulus Power dBm")
labels = ("Start (Hz)", "Stop (Hz)", "Points", "IF bandwidth (Hz)", "Stimulus power (dBm)")
edits = self._radar.sweep_edits
hint = self._radar.limits_hint
if limits is None:
@@ -3,9 +3,11 @@
import numpy as np
import pyqtgraph as pg
from python_app.gui.theme import PLOT_BACKGROUND
def create_plot(title: str = "") -> pg.PlotWidget:
plot = pg.PlotWidget(background="#0f141c", enableMenu=False, title=title)
plot = pg.PlotWidget(background=PLOT_BACKGROUND, enableMenu=False, title=title)
plot.showGrid(x=True, y=True, alpha=0.2)
return plot
@@ -20,4 +22,3 @@ def build_lut(stops: list[str], *, size: int = 256) -> np.ndarray:
for channel in range(3):
lut[:, channel] = np.interp(sample_positions, stop_positions, stop_colors[:, channel]).astype(np.uint8)
return lut
@@ -64,7 +64,6 @@ class GprRenderer:
def __init__(self) -> None:
self.widget = create_plot("GPR coherent BP")
self.widget.setBackground("#0f141c")
plot = self.widget.getPlotItem()
assert plot is not None
self._plot = plot
@@ -39,7 +39,7 @@ SAR3D_VIEWS: dict[str, tuple[str, str, str, tuple[int, int] | None]] = {
"sar3d_front": ("Front (X-Y)", "X", "Y", (0, 1)),
"sar3d_side": ("Side (Z-Y)", "Z", "Y", (2, 1)),
"sar3d_top": ("Top (X-Z)", "X", "Z", (0, 2)),
"sar3d_oblique": ("3D projection", "", "", None),
"sar3d_oblique": ("3D Projection", "", "", None),
}
# Corners of the scene box indexed by bits (x, y, z); an edge joins corners one bit apart.
@@ -209,10 +209,9 @@ class Sar3dRenderer:
(x0, x1), _, (z0, z1) = bounds
ground = (np.array([[x0, 0.0, z0], [x1, 0.0, z0], [x1, 0.0, z1], [x0, 0.0, z1], [x0, 0.0, z0]]) @ basis.table.T).T
self.ground.setData(ground[0], ground[1])
summary = f"SAR3D: ready — {status[1]}" if status else "SAR3D: ready"
if level is not None:
x, y, z = peak_xyz
self.plots["sar3d_oblique"].setTitle(
f"3D projection | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}"
)
self.status_label.setText(f"SAR3D: ready — {status[1]}" if status else "SAR3D: ready")
summary += f" | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}"
self.status_label.setText(summary)
return True
@@ -34,8 +34,9 @@ def build_data_actions_group(gui: GuiStateModel) -> DataView:
)
save_vna_json_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
remove_last_button = QPushButton("Remove Last Measurement")
remove_last_button.setProperty("role", "destructive")
remove_last_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
capture_tmp_reference_button = QPushButton("Capture Tmp Reference")
capture_tmp_reference_button = QPushButton("Capture Temporary Reference")
capture_tmp_reference_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
record_button = QPushButton("Start + Record to Disk")
record_button.setToolTip(
@@ -61,7 +62,7 @@ def build_data_actions_group(gui: GuiStateModel) -> DataView:
record_count_row = QHBoxLayout()
record_count_row.setSpacing(8)
record_count_row.addWidget(QLabel("Number of Sweeps to Record"))
record_count_row.addWidget(QLabel("Sweeps to record"))
record_count_row.addWidget(record_count)
record_count_row.addStretch(1)
layout.addLayout(record_count_row)
@@ -69,7 +70,7 @@ def build_data_actions_group(gui: GuiStateModel) -> DataView:
path_row = QHBoxLayout()
path_row.setSpacing(8)
save_path_input = view.add("_save_path_input", QLineEdit(str(data_defaults.save_path)))
browse_button = QPushButton("Browse")
browse_button = QPushButton("Browse…")
path_row.addWidget(QLabel("Path"))
path_row.addWidget(save_path_input, stretch=1)
path_row.addWidget(browse_button)
@@ -22,6 +22,7 @@ def build_two_column_form_widget(
form.setContentsMargins(0, 0, 0, 0)
form.setHorizontalSpacing(10)
form.setVerticalSpacing(6)
form.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
for row in column_rows:
if isinstance(row, tuple):
@@ -20,7 +20,7 @@ def build_preprocess_summary_group() -> PreprocessView:
for key in PREPROCESS_ASSET_NAMES:
label = QLabel("<not selected>")
view.labels[key] = label
rows.append((preprocess_asset_display_name(key), label))
rows.append((preprocess_asset_display_name(key).capitalize(), label))
layout.addWidget(build_two_column_form_widget(group, rows, split_index=(len(rows) + 1) // 2))
return view
@@ -1,16 +1,18 @@
"""Pinned primary actions; controllers connect their own buttons."""
from PyQt6.QtWidgets import QGridLayout, QGroupBox, QPushButton, QSizePolicy
from PyQt6.QtWidgets import QComboBox, QFormLayout, QGridLayout, QGroupBox, QPushButton, QSizePolicy, QWidget
from python_app.gui.controllers.sections.views import PrimaryView
def build_primary_actions_group() -> PrimaryView:
def build_primary_actions_group(mode: QComboBox) -> PrimaryView:
view = PrimaryView(group=QGroupBox("Actions"), start=QPushButton("Start"),
single_capture=QPushButton("Single Capture"), stop=QPushButton("Stop"),
apply_radar=QPushButton("Apply Radar"), load=QPushButton("Load Config"),
save=QPushButton("Save Config"), preprocess=QPushButton("Preprocessing"),
apply_radar=QPushButton("Apply Radar"), load=QPushButton("Load Config…"),
save=QPushButton("Save Config…"), preprocess=QPushButton("Preprocessing…"),
clear=QPushButton("Clear Runtime History"))
view.start.setProperty("role", "primary")
view.clear.setProperty("role", "destructive")
layout = QGridLayout(view.group)
layout.setHorizontalSpacing(8)
layout.setVerticalSpacing(8)
@@ -20,4 +22,9 @@ def build_primary_actions_group() -> PrimaryView:
layout.addWidget(button, index // 3, index % 3, 1, 2 if button is view.clear else 1)
for column in range(3):
layout.setColumnStretch(column, 1)
mode_row = QWidget(view.group)
mode_form = QFormLayout(mode_row)
mode_form.setContentsMargins(0, 0, 0, 0)
mode_form.addRow("Mode", mode)
layout.addWidget(mode_row, 3, 0, 1, 3)
return view
@@ -75,8 +75,8 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
unwrap_phase_checkbox,
("Switch combos", pass_through_combo_filter_input),
pass_through_fixed_y_enabled,
("Y min dB", pass_through_y_min_db),
("Y max dB", pass_through_y_max_db),
("Y min (dB)", pass_through_y_min_db),
("Y max (dB)", pass_through_y_max_db),
],
split_index=5,
)
@@ -98,11 +98,11 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
processing_mode_pages,
[
("Axis", bscan_axis),
("Cut m", bscan_cut_m),
("Max depth m", bscan_max_depth_m),
("Cut (m)", bscan_cut_m),
("Max depth (m)", bscan_max_depth_m),
("Gain", bscan_gain),
("Start MHz", bscan_start_freq_mhz),
("Stop MHz", bscan_stop_freq_mhz),
("Start (MHz)", bscan_start_freq_mhz),
("Stop (MHz)", bscan_stop_freq_mhz),
("Scans to show (stopped)", bscan_history_window),
bscan_subtract_mean_ascan,
],
@@ -188,27 +188,27 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
[
("Input positions", gpr_input_positions_input),
("Output positions", gpr_output_positions_input),
("Min depth m", gpr_min_depth_m),
("Max depth m", gpr_max_depth_m),
("Range comp power", gpr_range_comp_power),
("Object min frac", gpr_object_min_frac),
("Min depth (m)", gpr_min_depth_m),
("Max depth (m)", gpr_max_depth_m),
("Range compensation power", gpr_range_comp_power),
("Object threshold fraction", gpr_object_min_frac),
("Score mode", gpr_score_mode),
("Motion mode", gpr_motion_mode),
("Look angle deg", gpr_look_angle_deg),
("Look angle (°)", gpr_look_angle_deg),
("Direction sign", gpr_direction_sign),
gpr_ignore_socket_speed_enabled,
("Speed m/s", gpr_speed_m_s),
("Speed (m/s)", gpr_speed_m_s),
("Render mode", gpr_render_mode),
("Max detected objects", gpr_max_detected_objects_to_draw),
("Draw top M objects", gpr_draw_top_m_objects),
("Approach min frames", gpr_object_approach_min_frames),
("Start MHz", gpr_start_freq_mhz),
("Stop MHz", gpr_stop_freq_mhz),
("Imaging plane Y m", gpr_imaging_plane_y_m),
("Visible X min m", gpr_visible_x_min_m),
("Visible X max m", gpr_visible_x_max_m),
("Visible Z min m", gpr_visible_z_min_m),
("Visible Z max m", gpr_visible_z_max_m),
("Start (MHz)", gpr_start_freq_mhz),
("Stop (MHz)", gpr_stop_freq_mhz),
("Imaging plane Y (m)", gpr_imaging_plane_y_m),
("Visible X min (m)", gpr_visible_x_min_m),
("Visible X max (m)", gpr_visible_x_max_m),
("Visible Z min (m)", gpr_visible_z_min_m),
("Visible Z max (m)", gpr_visible_z_max_m),
gpr_background_subtract_enabled,
("Mean count", gpr_background_mean_count),
gpr_remove_sidelobe_objects_enabled,
@@ -259,24 +259,24 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
("Relative permittivity", gpr_relative_permittivity),
("Input positions", legacy_gpr_input_positions_input),
("Output positions", legacy_gpr_output_positions_input),
("Min depth m", legacy_gpr_min_depth_m),
("Max depth m", legacy_gpr_max_depth_m),
("Comp power", legacy_gpr_comp_power),
("SNR thresh", legacy_gpr_snr_thresh),
("SNR comp max", legacy_gpr_snr_comp_max),
("Speed m/s", legacy_gpr_speed_m_s),
("Min depth (m)", legacy_gpr_min_depth_m),
("Max depth (m)", legacy_gpr_max_depth_m),
("Compensation power", legacy_gpr_comp_power),
("SNR threshold", legacy_gpr_snr_thresh),
("Max SNR compensation", legacy_gpr_snr_comp_max),
("Speed (m/s)", legacy_gpr_speed_m_s),
legacy_gpr_ignore_socket_speed_enabled,
("Render mode", legacy_gpr_render_mode),
("Min visible pairs", legacy_gpr_min_visible_pair_count),
("Start MHz", legacy_gpr_start_freq_mhz),
("Stop MHz", legacy_gpr_stop_freq_mhz),
("Look angle deg", legacy_gpr_look_angle_deg),
("Start (MHz)", legacy_gpr_start_freq_mhz),
("Stop (MHz)", legacy_gpr_stop_freq_mhz),
("Look angle (°)", legacy_gpr_look_angle_deg),
("Reference mode", legacy_gpr_reference_mode),
legacy_gpr_apply_freq_phase_correction,
("Visible X min m", legacy_gpr_visible_x_min_m),
("Visible X max m", legacy_gpr_visible_x_max_m),
("Visible Z min m", legacy_gpr_visible_z_min_m),
("Visible Z max m", legacy_gpr_visible_z_max_m),
("Visible X min (m)", legacy_gpr_visible_x_min_m),
("Visible X max (m)", legacy_gpr_visible_x_max_m),
("Visible Z min (m)", legacy_gpr_visible_z_min_m),
("Visible Z max (m)", legacy_gpr_visible_z_max_m),
legacy_gpr_background_subtract_enabled,
("Mean count", legacy_gpr_background_mean_count),
],
@@ -345,7 +345,6 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
)
processing_mode_pages.addWidget(sar3d_page)
form.addRow("Mode", processing_mode)
form.addRow(processing_mode_pages)
form.addRow(gpr_common_page)
@@ -39,25 +39,20 @@ def build_radar_group(config: RunConfigModel) -> RadarView:
power_input = view.add("_power_input", QLineEdit(f"{defaults.sweep.power_dbm:g}"))
power_input.setToolTip("Stimulus power configured in the active profile.")
radar_start_label = QLabel("Start Hz")
radar_stop_label = QLabel("Stop Hz")
radar_points_label = QLabel("Points")
radar_ifbw_label = QLabel("IF BW Hz")
radar_power_label = QLabel("Stimulus Power dBm")
radar_labels = tuple(QLabel(text) for text in (
"Start (Hz)", "Stop (Hz)", "Points", "IF bandwidth (Hz)", "Stimulus power (dBm)",
))
for label in radar_labels:
label.setWordWrap(True)
radar_limits_hint = QLabel("Device limits are not available.")
radar_limits_hint.setObjectName("hintLabel")
radar_limits_hint.setWordWrap(True)
vna_layout.addWidget(
build_two_column_form_widget(
group,
[
(radar_start_label, start_hz_input),
(radar_stop_label, stop_hz_input),
(radar_points_label, points_input),
(radar_ifbw_label, ifbw_input),
(radar_power_label, power_input),
],
list(zip(radar_labels, (start_hz_input, stop_hz_input, points_input, ifbw_input, power_input))),
)
)
vna_layout.addWidget(radar_limits_hint)
@@ -71,7 +66,7 @@ def build_radar_group(config: RunConfigModel) -> RadarView:
adc_radar_settings_panel.setVisible(config.is_kamil_adc)
return RadarView(group=group, fields=view.fields,
sweep_edits=(start_hz_input, stop_hz_input, points_input, ifbw_input, power_input),
sweep_labels=(radar_start_label, radar_stop_label, radar_points_label, radar_ifbw_label, radar_power_label),
sweep_labels=radar_labels,
limits_hint=radar_limits_hint, vna_panel=vna_radar_settings_panel, adc_panel=adc_radar_settings_panel,
optical_enabled=optical_enabled, optical_mode=optical_mode, optical_common=optical_common,
optical_manual=optical_manual, optical_variation=optical_variation,
@@ -144,9 +139,9 @@ def _build_adc_settings_panel(view: SectionView, config: RunConfigModel):
("TTY path", adc_tty_path_input),
("Arguments", adc_args_input),
("Environment JSON", adc_env_input),
("Startup timeout s", adc_startup_timeout_s_input),
("Read timeout s", adc_sweep_timeout_s_input),
("Stop timeout s", adc_stop_timeout_s_input),
("Startup timeout (s)", adc_startup_timeout_s_input),
("Read timeout (s)", adc_sweep_timeout_s_input),
("Stop timeout (s)", adc_stop_timeout_s_input),
("Board enabled", optical_enabled_checkbox),
("Board port", optical_port_input),
("Board mode", optical_mode_combo),
@@ -29,7 +29,7 @@ def build_switch_group(config: RunConfigModel, gui: GuiStateModel) -> SwitchView
global_form = QFormLayout()
global_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
settling_ms = view.add("_settling_ms", QLineEdit(str(config.runtime.settling_ms)))
global_form.addRow("Settling ms", settling_ms)
global_form.addRow("Settling (ms)", settling_ms)
layout.addLayout(global_form)
combos_text = view.add("_combos_text", QLineEdit(str(switch_defaults.combos_text)))
@@ -71,7 +71,7 @@ def build_switch_group(config: RunConfigModel, gui: GuiStateModel) -> SwitchView
display_combo = view.add("_display_combo", QComboBox())
display_combo.setEnabled(False)
display_row.addWidget(display_combo, stretch=1)
next_combo_button = QPushButton("Next combination")
next_combo_button = QPushButton("Next Combination")
next_combo_button.setEnabled(False)
display_row.addWidget(next_combo_button)
layout.addLayout(display_row)