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
+3 -3
View File
@@ -53,8 +53,8 @@ class AntennaGeometryEditor(QWidget):
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._table)
buttons = QHBoxLayout()
self._add_button = QPushButton("Add antenna", self)
self._remove_button = QPushButton("Remove selected", self)
self._add_button = QPushButton("Add Antenna", self)
self._remove_button = QPushButton("Remove Selected", self)
buttons.addWidget(self._add_button)
buttons.addWidget(self._remove_button)
layout.addLayout(buttons)
@@ -104,7 +104,7 @@ class AntennaGeometryEditor(QWidget):
except (ValueError, RuntimeError) as exc:
with QSignalBlocker(self):
self.set_records(self._valid_records)
QMessageBox.warning(self, "Invalid antenna geometry", str(exc))
QMessageBox.warning(self, "Invalid Antenna Geometry", str(exc))
return
self._pending_row = None
self._valid_records = records
@@ -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)
+29 -19
View File
@@ -10,11 +10,12 @@ import pyqtgraph as pg
from PyQt6.QtCore import QSignalBlocker, QRectF, Qt, pyqtSignal
from PyQt6.QtWidgets import (
QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFormLayout, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QScrollArea, QTableWidget, QTableWidgetItem,
QToolButton, QVBoxLayout, QWidget,
)
from python_app.models.dataset_model import SweepCollection
from python_app.gui.theme import PLOT_BACKGROUND
class ObjectCalibrationDialog(QDialog):
@@ -33,7 +34,13 @@ class ObjectCalibrationDialog(QDialog):
self._provenance: dict | None = None
self._capture_pending = self._calculating = False
self._calibration: dict | None = None
layout = QVBoxLayout(self)
root_layout = QVBoxLayout(self)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
content = QWidget(scroll)
layout = QVBoxLayout(content)
scroll.setWidget(content)
root_layout.addWidget(scroll)
name_row = QHBoxLayout()
name_row.addWidget(QLabel("Set name", self))
self._set_name_input = QLineEdit(set_name, self)
@@ -49,6 +56,7 @@ class ObjectCalibrationDialog(QDialog):
explanation.setWordWrap(True)
layout.addWidget(explanation)
self._table = QTableWidget(0, 5, self)
self._table.setMaximumHeight(140)
self._table.setHorizontalHeaderLabels(["Target", "Sweeps", "X (m)", "Y (m)", "Z (m)"])
horizontal, vertical = self._table.horizontalHeader(), self._table.verticalHeader()
assert horizontal is not None and vertical is not None
@@ -57,9 +65,10 @@ class ObjectCalibrationDialog(QDialog):
self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
layout.addWidget(self._table)
actions = QHBoxLayout()
self._capture_button = QPushButton("Capture data", self)
self._capture_button = QPushButton("Capture Data", self)
self._capture_button.clicked.connect(self.capture_requested.emit)
self._remove_button = QPushButton("Remove selected target", self)
self._remove_button = QPushButton("Remove Selected Target", self)
self._remove_button.setProperty("role", "destructive")
self._remove_button.clicked.connect(self._remove_capture)
actions.addWidget(self._capture_button)
actions.addWidget(self._remove_button)
@@ -68,27 +77,27 @@ class ObjectCalibrationDialog(QDialog):
self._settings: dict[str, QDoubleSpinBox | QCheckBox | QComboBox] = {}
form = QFormLayout()
for key, title, default, low, high in (
("sphere_radius_m", "Sphere radius, m (0 = point)", .16, 0, 100),
("antenna_height_m", "Antenna origin height above ground, m", .92, 0, 100),
("pitch_deg", "Radar downward tilt, °", 10, -90, 90),
("sphere_radius_m", "Sphere radius (m, 0 = point)", .16, 0, 100),
("antenna_height_m", "Antenna origin height above ground (m)", .92, 0, 100),
("pitch_deg", "Radar downward tilt (°)", 10, -90, 90),
):
form.addRow(title, self._number(key, default, low, high))
layout.addLayout(form)
# A disclosure header: only the header shows until the operator opens it.
advanced_toggle = QToolButton(self)
advanced_toggle.setText("Calculation settings")
advanced_toggle.setText("Calculation Settings")
advanced_toggle.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
advanced_toggle.setArrowType(Qt.ArrowType.RightArrow)
advanced_toggle.setAutoRaise(True)
advanced = QGroupBox(self)
advanced_form = QFormLayout(advanced)
for key, title, default, low, high in (
("f_min", "Estimation band start, MHz", 2000, 0, 100000),
("f_max", "Estimation band stop, MHz", 6000, 0, 100000),
("gate_half_m", "Echo path gate half-width, m", .15, .000001, 100),
("gate_taper_m", "Gate taper width, m", .03, .000001, 100),
("coarse_range_search_m", "Coarse path search, ±m (0 = off)", .6, 0, 100),
("refine_range_m", "Coordinate refinement, ±m", .1, 0, 100),
("f_min", "Estimation band start (MHz)", 2000, 0, 100000),
("f_max", "Estimation band stop (MHz)", 6000, 0, 100000),
("gate_half_m", "Echo path gate half-width (m)", .15, .001, 100),
("gate_taper_m", "Gate taper width (m)", .03, .001, 100),
("coarse_range_search_m", "Coarse path search (±m, 0 = off)", .6, 0, 100),
("refine_range_m", "Coordinate refinement (±m)", .1, 0, 100),
):
advanced_form.addRow(title, self._number(key, default, low, high))
axes = QWidget(advanced)
@@ -123,7 +132,7 @@ class ObjectCalibrationDialog(QDialog):
self._target_combo = QComboBox(self)
layout.addWidget(self._target_combo)
self._phase_view = pg.GraphicsLayoutWidget(parent=self)
self._phase_view.setBackground("#0f141c")
self._phase_view.setBackground(PLOT_BACKGROUND)
self._phase_view.setMinimumHeight(270)
self._phase_plots = []
self._phase_images = []
@@ -154,9 +163,10 @@ class ObjectCalibrationDialog(QDialog):
self._status_label.setWordWrap(True)
layout.addWidget(self._status_label)
footer = QHBoxLayout()
self._calculate_button = QPushButton("Calculate and save", self)
self._calculate_button = QPushButton("Calculate and Save", self)
self._calculate_button.setProperty("role", "primary")
self._calculate_button.clicked.connect(self.calculate_requested.emit)
self._cancel_button = QPushButton("Cancel operation", self)
self._cancel_button = QPushButton("Cancel Operation", self)
self._cancel_button.clicked.connect(self.cancel_requested.emit)
close_button = QPushButton("Close", self)
close_button.clicked.connect(self.reject)
@@ -164,7 +174,7 @@ class ObjectCalibrationDialog(QDialog):
footer.addWidget(self._cancel_button)
footer.addStretch(1)
footer.addWidget(close_button)
layout.addLayout(footer)
root_layout.addLayout(footer)
self._table.itemChanged.connect(self._measurements_changed)
self._target_combo.currentIndexChanged.connect(self._draw_phase_effect)
for field in [*self._settings.values(), *self._refine_axes.values()]:
@@ -175,7 +185,7 @@ class ObjectCalibrationDialog(QDialog):
def _number(self, key: str, value: float, low: float, high: float) -> QDoubleSpinBox:
field = QDoubleSpinBox(self)
field.setDecimals(6)
field.setDecimals(0 if key in {"f_min", "f_max"} else 1 if key == "pitch_deg" else 3)
field.setRange(low, high)
field.setSingleStep(100 if key in {"f_min", "f_max"} else .1 if key == "pitch_deg" else .01)
field.setValue(value)
+14 -20
View File
@@ -26,6 +26,7 @@ from PyQt6.QtWidgets import (
import numpy as np
import pyqtgraph as pg
from python_app.gui.controllers.renderers.common import create_plot
from python_app.models.gui_profile_schema import GuiPreprocessDialogStateModel
from python_app.models.dataset_model import TraceData
from python_app.orchestration.preprocess_assets import (
@@ -113,7 +114,7 @@ class PreprocessDialog(QDialog):
layout.addLayout(self._build_median_sweep_row(group))
layout.addWidget(self._build_radar_config_group(group))
layout.addWidget(self._build_selector_group("S21", tuple(PREPROCESS_ASSET_NAMES), group))
layout.addLayout(self._build_selectors(group))
return group
def _build_median_sweep_row(self, parent: QGroupBox) -> QHBoxLayout:
@@ -141,7 +142,7 @@ class PreprocessDialog(QDialog):
self._radar_config_dir_input = QLineEdit(group)
self._radar_config_dir_input.setPlaceholderText("Directory with radar JSON configs")
self._radar_config_dir_input.editingFinished.connect(self.radar_config_dir_changed.emit)
browse_button = QPushButton("Browse...", group)
browse_button = QPushButton("Browse…", group)
browse_button.clicked.connect(self._choose_radar_config_dir)
dir_row.addWidget(QLabel("Directory"))
dir_row.addWidget(self._radar_config_dir_input, stretch=1)
@@ -160,14 +161,13 @@ class PreprocessDialog(QDialog):
layout.addWidget(self._use_all_radar_configs_checkbox)
return group
def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox:
"""Build one selector subgroup for a channel family."""
group = QGroupBox(title, parent)
form = QFormLayout(group)
def _build_selectors(self, parent: QGroupBox) -> QFormLayout:
"""Build the S21 set selectors in capture order."""
form = QFormLayout()
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
for key in keys:
combo = QComboBox(group)
for key in PREPROCESS_ASSET_NAMES:
combo = QComboBox(parent)
combo.setPlaceholderText("<not selected>")
combo.setProperty("optional", key not in REQUIRED_PREPROCESS_ASSET_KEYS)
self._set_combo_items(combo, [], "")
@@ -175,7 +175,7 @@ class PreprocessDialog(QDialog):
self._set_combos[key] = combo
field = combo
if key == "s21_object_calibration":
field = QWidget(group)
field = QWidget(parent)
row = QHBoxLayout(field)
row.setContentsMargins(0, 0, 0, 0)
row.addWidget(combo, stretch=1)
@@ -183,12 +183,12 @@ class PreprocessDialog(QDialog):
self._create_object_calibration_button.setToolTip("Calculate a MIMO calibration from reflector measurements.")
self._create_object_calibration_button.clicked.connect(self.create_object_calibration_requested.emit)
row.addWidget(self._create_object_calibration_button)
form.addRow(self._asset_row_label(key), field)
return group
form.addRow(preprocess_asset_display_name(key).capitalize(), field)
return form
def _build_sequence_group(self) -> QGroupBox:
"""Build sequential-capture controls and capture log."""
group = QGroupBox("Sequential Capture (Fill Full N*M)", self)
group = QGroupBox("Capture All Antenna Pairs", self)
layout = QGridLayout(group)
self._active_kind_label = QLabel("<none>", group)
@@ -239,6 +239,7 @@ class PreprocessDialog(QDialog):
self._save_sequence_button.setEnabled(False)
self._abort_button = QPushButton("Abort Sequence", parent)
self._abort_button.setProperty("role", "destructive")
self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
self._abort_button.setEnabled(False)
@@ -270,7 +271,6 @@ class PreprocessDialog(QDialog):
placeholder = QLabel("Preview will appear after the first successful capture.", host)
placeholder.setWordWrap(True)
placeholder.setMinimumHeight(320)
layout.addWidget(placeholder)
self._preview_host_layout = layout
@@ -531,8 +531,7 @@ class PreprocessDialog(QDialog):
@staticmethod
def _build_preview_axis(left_label: str, left_units: str) -> "pg.PlotWidget":
"""Return one PlotWidget configured with the project's preview style."""
plot = pg.PlotWidget(background="#101418", enableMenu=False)
plot.showGrid(x=True, y=True, alpha=0.2)
plot = create_plot()
plot.setLabel("bottom", "Frequency", units="Hz")
plot.setLabel("left", left_label, units=left_units)
plot.setMinimumHeight(320)
@@ -554,11 +553,6 @@ class PreprocessDialog(QDialog):
self._radar_config_dir_input.setText(selected_dir)
self.radar_config_dir_changed.emit()
@staticmethod
def _asset_row_label(key: str) -> str:
"""Return short selector label for one preprocess asset."""
return preprocess_asset_display_name(key)
@staticmethod
def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None:
"""Replace combo contents and keep previous value when still available."""
+20 -63
View File
@@ -5,23 +5,15 @@ from __future__ import annotations
from PyQt6.QtGui import QColor, QPalette
from PyQt6.QtWidgets import QApplication, QStyleFactory
PLOT_BACKGROUND = "#0f141c"
_LIGHT_STYLESHEET = """
QMainWindow, QDialog {
background-color: #f3f6fb;
}
QWidget {
color: #1f2937;
font-size: 13px;
}
QGroupBox {
background-color: #ffffff;
border: 1px solid #d7dee8;
border-radius: 10px;
margin-top: 14px;
padding: 10px;
border-radius: 6px;
margin-top: 12px;
padding: 6px;
}
QGroupBox::title {
@@ -35,11 +27,11 @@ QGroupBox::title {
QPushButton {
background-color: #f8fafc;
border: 1px solid #cad4e1;
border-radius: 8px;
padding: 7px 12px;
border-radius: 4px;
padding: 3px 10px;
}
QPushButton:hover {
QPushButton:hover:enabled {
background-color: #eef3f9;
}
@@ -47,70 +39,30 @@ QPushButton:pressed {
background-color: #e4ebf4;
}
QPushButton:checked {
QPushButton:checked, QPushButton[role="primary"] {
background-color: #2f7ee6;
border-color: #2f7ee6;
color: #ffffff;
}
QPushButton:checked:hover {
QPushButton:checked:hover:enabled, QPushButton[role="primary"]:hover:enabled {
background-color: #3b8bf4;
}
QPushButton[role="destructive"] {
color: #a73545;
border-color: #cf9ba4;
}
QPushButton:disabled {
color: #98a4b3;
background-color: #f3f5f8;
border-color: #dde4ec;
}
QPushButton#settingsToggleButton {
min-width: 26px;
max-width: 26px;
padding: 6px 0px;
border-radius: 7px;
font-weight: 700;
}
QPushButton#sectionToggleButton {
min-width: 84px;
padding: 5px 10px;
border-radius: 7px;
}
QLineEdit,
QPlainTextEdit,
QTextEdit,
QComboBox,
QSpinBox,
QDoubleSpinBox {
background-color: #ffffff;
border: 1px solid #c9d4e1;
border-radius: 7px;
padding: 5px 8px;
selection-background-color: #cfe3ff;
}
QLineEdit:focus,
QPlainTextEdit:focus,
QTextEdit:focus,
QComboBox:focus,
QSpinBox:focus,
QDoubleSpinBox:focus {
border: 1px solid #2f7ee6;
}
QTextEdit#runtimeLogBox {
font-family: "DejaVu Sans Mono";
font-size: 12px;
padding: 6px 8px;
color: #1f2937;
background-color: #fbfdff;
border-color: #c9d4e1;
}
QComboBox::drop-down {
border: none;
width: 18px;
}
QScrollArea {
@@ -121,7 +73,6 @@ QScrollArea {
QLabel#statusLabel {
color: #35507a;
font-weight: 600;
padding: 2px 1px;
}
QLabel#hintLabel {
@@ -133,6 +84,9 @@ QLabel#hintLabel {
def apply_light_theme(app: QApplication) -> None:
"""Apply a minimal light theme shared by all windows."""
app.setStyle(QStyleFactory.create("Fusion"))
font = app.font()
font.setPointSize(10)
app.setFont(font)
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor("#f3f6fb"))
@@ -148,5 +102,8 @@ def apply_light_theme(app: QApplication) -> None:
palette.setColor(QPalette.ColorRole.Link, QColor("#2f7ee6"))
palette.setColor(QPalette.ColorRole.Highlight, QColor("#2f7ee6"))
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
for role in (QPalette.ColorRole.Text, QPalette.ColorRole.ButtonText, QPalette.ColorRole.WindowText):
palette.setColor(QPalette.ColorGroup.Disabled, role, QColor("#98a4b3"))
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor("#f3f5f8"))
app.setPalette(palette)
app.setStyleSheet(_LIGHT_STYLESHEET)
+2 -4
View File
@@ -17,12 +17,10 @@ from PyQt6.QtCore import QRectF
from pyqtgraph.exporters import ImageExporter
from python_app.models.dataset_model import TraceData
from python_app.gui.theme import PLOT_BACKGROUND
logger = logging.getLogger(__name__)
# Matches the preview pane styling in preprocess_dialog so saved PNGs and the
# on-screen preview look the same.
_BACKGROUND = "#101418"
_MAGNITUDE_PEN = pg.mkPen("#4cc9f0", width=1.8)
_PHASE_PEN = pg.mkPen("#f48c06", width=1.8)
_EXPORT_SIZE = (1400, 520)
@@ -45,7 +43,7 @@ def export_trace_png(
phase_deg = np.degrees(np.angle(samples))
layout = pg.GraphicsLayoutWidget(show=False, size=_EXPORT_SIZE)
layout.setBackground(_BACKGROUND)
layout.setBackground(PLOT_BACKGROUND)
try:
layout.ci.addLabel(title, row=0, col=0, colspan=2)
+14 -10
View File
@@ -2,9 +2,10 @@
from pathlib import Path
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QFileDialog, QFrame, QHBoxLayout, QLabel, QMainWindow, QPushButton,
QScrollArea, QVBoxLayout, QWidget,
QFileDialog, QFrame, QHBoxLayout, QLabel, QMainWindow,
QScrollArea, QToolButton, QVBoxLayout, QWidget,
)
from python_app.gui.controllers.history_controller import HistoryController
@@ -29,9 +30,9 @@ class WindowView:
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(14)
layout.addWidget(plot_stack, stretch=11)
self._toggle = QPushButton(">")
self._toggle.setObjectName("settingsToggleButton")
self._toggle.setFixedWidth(26)
self._toggle = QToolButton()
self._toggle.setAutoRaise(True)
self._toggle.setArrowType(Qt.ArrowType.RightArrow)
self._toggle.setToolTip("Hide settings panel")
self._toggle.clicked.connect(self.toggle_settings)
layout.addWidget(self._toggle)
@@ -40,12 +41,12 @@ class WindowView:
settings_layout = QVBoxLayout(self.settings_panel)
settings_layout.setContentsMargins(0, 0, 0, 0)
settings_layout.setSpacing(10)
self.primary = build_primary_actions_group()
self.processing = build_processing_group(config, gui)
self.primary = build_primary_actions_group(self.processing.mode)
settings_layout.addWidget(self.primary.group)
self.data = build_data_actions_group(gui)
self.switches = build_switch_group(config, gui)
self.preprocess = build_preprocess_summary_group()
self.processing = build_processing_group(config, gui)
self.radar = build_radar_group(config)
self.data.browse.clicked.connect(lambda: self.browse_save_path(project_root))
self.sections = (self.data, self.switches, self.preprocess, self.processing, self.radar)
@@ -63,10 +64,13 @@ class WindowView:
settings_layout.addWidget(scroll, stretch=1)
self.status = QLabel("Status: idle")
self.status.setObjectName("statusLabel")
self.status.setWordWrap(True)
self._history = QLabel("History: raw=0, preprocessed=0, results=0")
self._history.setObjectName("hintLabel")
settings_layout.addWidget(self.status)
settings_layout.addWidget(self._history)
status_row = QHBoxLayout()
status_row.addWidget(self.status, stretch=1)
status_row.addWidget(self._history, alignment=Qt.AlignmentFlag.AlignRight)
settings_layout.addLayout(status_row)
settings_layout.addWidget(log_panel)
layout.addWidget(self.settings_panel, stretch=7)
parent.resize(1650, 940)
@@ -74,7 +78,7 @@ class WindowView:
def toggle_settings(self) -> None:
visible = self.settings_panel.isHidden()
self.settings_panel.setVisible(visible)
self._toggle.setText(">" if visible else "<")
self._toggle.setArrowType(Qt.ArrowType.RightArrow if visible else Qt.ArrowType.LeftArrow)
self._toggle.setToolTip("Hide settings panel" if visible else "Show settings panel")
def update_history(self, history: HistoryController) -> None: