Files
radar_system/python_app/gui/antenna_geometry_editor.py
T
2026-10-06 12:32:26 +03:00

141 lines
6.4 KiB
Python

"""Table editor for antenna switch positions and coordinates in metres."""
from __future__ import annotations
from collections.abc import Callable
from typing import cast
from PyQt6.QtCore import QItemSelectionModel, QLocale, QSignalBlocker, pyqtSignal
from PyQt6.QtGui import QDoubleValidator, QIntValidator
from PyQt6.QtWidgets import (
QAbstractItemView, QHeaderView, QHBoxLayout, QLineEdit, QMessageBox, QPushButton,
QStyledItemDelegate, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
)
from python_app.models.run_config_validation import normalize_antenna_geometry
class _CoordinateDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
validator = (
QIntValidator(0, cast(QTableWidget, self.parent()).property("position_count") - 1, editor)
if index.column() == 0 else QDoubleValidator(editor)
)
validator.setLocale(QLocale.c())
editor.setValidator(validator)
return editor
class AntennaGeometryEditor(QWidget):
"""Keep geometry explicit and preserve all three coordinates on profile round trips."""
changed = pyqtSignal()
def __init__(self, position_field: str, entries, *, position_count: int,
validate_change: Callable[[], object] | None = None, parent=None) -> None:
super().__init__(parent)
self.position_field = position_field
self.validate_change = validate_change
self._pending_row: int | None = None
self._table = QTableWidget(0, 4, self)
self.set_position_count(position_count)
self._table.setHorizontalHeaderLabels([
"Output" if position_field == "output_pos" else "Input", "X (m)", "Y (m)", "Z (m)",
])
cast(QHeaderView, self._table.horizontalHeader()).setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
cast(QHeaderView, self._table.verticalHeader()).hide()
self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self._table.setItemDelegate(_CoordinateDelegate(self._table))
self._table.setMinimumHeight(130)
self._table.setMaximumHeight(220)
layout = QVBoxLayout(self)
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)
buttons.addWidget(self._add_button)
buttons.addWidget(self._remove_button)
layout.addLayout(buttons)
self.set_records(entries)
self._table.itemChanged.connect(self._on_item_changed)
self._add_button.clicked.connect(self._add_row)
self._remove_button.clicked.connect(self._remove_rows)
def records(self) -> list[dict]:
records = []
for row in range(self._table.rowCount()):
if row == self._pending_row and not cast(QTableWidgetItem, self._table.item(row, 1)).text().strip():
continue
record = {}
for column, key in enumerate((self.position_field, "x_m", "y_m", "z_m")):
text = cast(QTableWidgetItem, self._table.item(row, column)).text()
try:
record[key] = int(text) if column == 0 else float(text)
except ValueError as exc:
raise ValueError(f"Antenna row {row + 1}: {key} must be {'an integer' if column == 0 else 'a number'}") from exc
records.append(record)
return normalize_antenna_geometry(records, self.position_field, self.position_count)
def set_position_count(self, count: int) -> None:
self.position_count = int(count)
self._table.setProperty("position_count", int(count))
def set_records(self, entries) -> None:
records = normalize_antenna_geometry(entries, self.position_field, self.position_count)
with QSignalBlocker(self._table):
self._table.setRowCount(len(records))
for row, record in enumerate(records):
for column, value in enumerate(record.values()):
self._table.setItem(row, column, QTableWidgetItem(str(value)))
self._pending_row = None
self._valid_records = records
self._add_button.setEnabled(len(records) < self._table.property("position_count"))
self.changed.emit()
def _on_item_changed(self, _item) -> None:
try:
records = self.records()
if records == self._valid_records:
return
if self.validate_change is not None:
self.validate_change()
except (ValueError, RuntimeError) as exc:
with QSignalBlocker(self):
self.set_records(self._valid_records)
QMessageBox.warning(self, "Invalid antenna geometry", str(exc))
return
self._pending_row = None
self._valid_records = records
self._add_button.setEnabled(len(records) < self.position_count)
self.changed.emit()
def _add_row(self) -> None:
row = self._table.rowCount()
if row >= self.position_count or self._pending_row is not None:
return
positions = {record[self.position_field] for record in self.records()}
position = next(index for index in range(self.position_count) if index not in positions)
with QSignalBlocker(self._table):
self._table.insertRow(row)
for column, value in enumerate((str(position), "", "0", "0")):
self._table.setItem(row, column, QTableWidgetItem(value))
self._pending_row = row
self._add_button.setEnabled(False)
self._table.setCurrentCell(row, 1)
self._table.editItem(self._table.item(row, 1))
def _remove_rows(self) -> None:
rows = {index.row() for index in cast(QItemSelectionModel, self._table.selectionModel()).selectedRows()}
if not rows:
return
with QSignalBlocker(self._table):
for row in sorted(rows, reverse=True):
self._table.removeRow(row)
if self._pending_row is not None:
self._pending_row = (None if self._pending_row in rows else
self._pending_row - sum(row < self._pending_row for row in rows))
self._on_item_changed(None)
self._add_button.setEnabled(self._pending_row is None and self._table.rowCount() < self.position_count)