141 lines
6.4 KiB
Python
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)
|