Polish GUI design
This commit is contained in:
@@ -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)}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -51,7 +51,8 @@ class Sar3dPlotTest(unittest.TestCase):
|
||||
|
||||
def test_all_four_projections_have_visible_images_and_correct_peak_axes(self) -> None:
|
||||
self.assertTrue(self.renderer.draw(_projections()))
|
||||
self.assertEqual(self.renderer.status_label.text(), "SAR3D: ready — CUDA")
|
||||
self.assertEqual(self.renderer.status_label.text(),
|
||||
"SAR3D: ready — CUDA | max X +0.50, Y -0.20, Z 2.00 m | level 2")
|
||||
positions = {"sar3d_front": (0.5, -0.2), "sar3d_side": (2, -0.2),
|
||||
"sar3d_top": (0.5, 2), "sar3d_oblique": (0.5, -0.2)}
|
||||
for name, position in positions.items():
|
||||
|
||||
@@ -117,7 +117,7 @@ btnNextCombo.addEventListener("click", () => changeDisplayCombo("/api/next_combo
|
||||
settingsToggle.addEventListener("click", () => sidePanel.classList.toggle("collapsed"));
|
||||
|
||||
// The form is built ENTIRELY from the schema the server derives from the Qt widgets
|
||||
// (field, group, kind, options, ranges, value, enabled). The web hardcodes nothing
|
||||
// (field, label, group, kind, options, ranges, value, enabled). The web hardcodes nothing
|
||||
// and shows only the active mode's fields, so it always mirrors the desktop.
|
||||
const fieldInputs = {}; // field name -> { el, kind, dirty }
|
||||
let formSignature = ""; // field names currently in the form (detect mode/structure change)
|
||||
@@ -146,7 +146,7 @@ function makeGeometryEditor(entry) {
|
||||
input.type = "number";
|
||||
input.step = index === 0 ? "1" : "any";
|
||||
input.required = true;
|
||||
input.setAttribute("aria-label", `${entry.name} ${key}`);
|
||||
input.setAttribute("aria-label", `${entry.label} ${key}`);
|
||||
if (index === 0) { input.min = "0"; input.max = entry.position_count - 1; }
|
||||
input.value = record[key] ?? 0;
|
||||
input.disabled = entry.enabled === false;
|
||||
@@ -177,7 +177,7 @@ function makeGeometryEditor(entry) {
|
||||
};
|
||||
el.setRecords(entry.value);
|
||||
add.type = "button";
|
||||
add.textContent = "Add antenna";
|
||||
add.textContent = "Add Antenna";
|
||||
add.addEventListener("click", () => {
|
||||
if (body.rows.length >= entry.position_count) return;
|
||||
const positions = new Set(Array.from(body.rows, (row) => Number(row.querySelector("input").value)));
|
||||
@@ -196,7 +196,7 @@ function makeFieldRow(entry) {
|
||||
row.className = "field";
|
||||
if (entry.kind === "geometry") row.classList.add("geometry-field");
|
||||
const label = document.createElement("label");
|
||||
label.textContent = entry.kind === "geometry" ? (entry.name === "tx_geometry" ? "Tx antenna positions" : "Rx antenna positions") : entry.name;
|
||||
label.textContent = entry.label;
|
||||
label.htmlFor = "f_" + entry.name;
|
||||
row.appendChild(label);
|
||||
|
||||
|
||||
@@ -10,21 +10,21 @@
|
||||
<header class="topbar">
|
||||
<div class="brand">Radar System</div>
|
||||
<div class="controls">
|
||||
<button id="btn-start" class="btn">Start</button>
|
||||
<button id="btn-start" class="btn primary">Start</button>
|
||||
<button id="btn-single" class="btn">Single Capture</button>
|
||||
<button id="btn-stop" class="btn">Stop</button>
|
||||
<button id="btn-tmp-ref" class="btn">Tmp Reference</button>
|
||||
<button id="btn-remove-last" class="btn">Remove Last Measurement</button>
|
||||
<button id="btn-tmp-ref" class="btn">Capture Temporary Reference</button>
|
||||
<button id="btn-remove-last" class="btn destructive">Remove Last Measurement</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="layout">
|
||||
<section class="plot-panel">
|
||||
<div class="plot-head">
|
||||
<span class="plot-title">Processor output</span>
|
||||
<span class="plot-title">Processor Output</span>
|
||||
<div id="display-combo-controls" class="config-row" hidden>
|
||||
<select id="display-combo" class="config-control" aria-label="B-scan channel combination"></select>
|
||||
<button id="btn-next-combo" class="btn">Next combination</button>
|
||||
<button id="btn-next-combo" class="btn">Next Combination</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sar3d-status" class="sar3d-status" role="status" hidden></div>
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<aside class="side-panel">
|
||||
<div class="config-section">
|
||||
<div class="config-title">Config profile</div>
|
||||
<div class="config-title">Config Profile</div>
|
||||
<div class="config-row">
|
||||
<select id="config-select" class="config-control" aria-label="Run config"></select>
|
||||
<button id="btn-load-config" class="btn">Load</button>
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<div class="config-title">Save / record dataset</div>
|
||||
<div class="config-title">Save / Record Dataset</div>
|
||||
<div class="config-row">
|
||||
<input id="save-path" class="config-control" type="text" placeholder="Save path" aria-label="Save path" />
|
||||
</div>
|
||||
@@ -55,20 +55,20 @@
|
||||
<div class="config-row">
|
||||
<input id="record-count" class="config-control" type="number" min="1" step="1"
|
||||
placeholder="Sweeps to record" aria-label="Number of sweeps to record" />
|
||||
<button id="btn-start-recording" class="btn">Start + Record</button>
|
||||
<button id="btn-start-recording" class="btn">Start + Record to Disk</button>
|
||||
</div>
|
||||
<div class="config-active" id="recording-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-head" id="settings-toggle">
|
||||
<span class="panel-title">Processor settings</span>
|
||||
<span class="panel-title">Processor Settings</span>
|
||||
<span class="chevron" id="settings-chevron">▾</span>
|
||||
</div>
|
||||
<div class="panel-body" id="settings-body">
|
||||
<div id="settings-fields" class="settings-fields"></div>
|
||||
<div class="settings-actions">
|
||||
<button id="btn-apply" class="btn primary">Apply</button>
|
||||
<button id="btn-reset-history" class="btn">Reset history</button>
|
||||
<button id="btn-reset-history" class="btn destructive">Clear Runtime History</button>
|
||||
</div>
|
||||
<div id="settings-note" class="note"></div>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +78,7 @@ body {
|
||||
color: #ffffff;
|
||||
}
|
||||
.btn.primary:hover:not(:disabled) { background: var(--accent-hover); }
|
||||
.btn.destructive { color: #a73545; border-color: #cf9ba4; }
|
||||
.btn:disabled {
|
||||
color: var(--disabled-text);
|
||||
background: var(--disabled-bg);
|
||||
@@ -125,6 +126,7 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#plot { display: block; width: 100%; height: 100%; object-fit: contain; }
|
||||
#plot:not([src]) { display: none; }
|
||||
|
||||
/* Side panel */
|
||||
.side-panel {
|
||||
@@ -154,7 +156,7 @@ body {
|
||||
border-radius: 7px;
|
||||
padding: 5px 7px;
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.config-control:focus { outline: none; border-color: var(--accent); }
|
||||
@@ -197,8 +199,7 @@ body {
|
||||
.group-title {
|
||||
margin: 12px 0 6px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: capitalize;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -227,7 +228,7 @@ body {
|
||||
border-radius: 7px;
|
||||
padding: 4px 7px;
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.field input:focus,
|
||||
@@ -251,7 +252,7 @@ body {
|
||||
border-radius: 7px;
|
||||
padding: 4px 7px;
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.field textarea:focus { outline: none; border-color: var(--accent); }
|
||||
@@ -262,7 +263,7 @@ body {
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
.settings-actions .btn { flex: 1; }
|
||||
.settings-actions .btn { flex: auto; white-space: nowrap; }
|
||||
.note {
|
||||
padding: 0 12px 10px;
|
||||
color: var(--muted);
|
||||
|
||||
Reference in New Issue
Block a user