sky tab: replace per-satellite trail toggle with global 'Show Trails' checkbox

Removed Trail column from satellite table. Added a single 'Show Trails'
checkbox in the source selector box that enables/disables all trails at once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 14:55:31 +02:00
co-authored by Claude Sonnet 4.6
parent 1522a2eb6a
commit 7fe6c34fd2
+30 -74
View File
@@ -1,4 +1,4 @@
import sys, os, time import sys, os
from collections import deque from collections import deque
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -10,7 +10,7 @@ for _p in [_root, _gui]:
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QListWidget,
QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView, QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView,
QSplitter, QGroupBox, QFrame, QSplitter, QGroupBox, QFrame, QCheckBox,
) )
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QBrush from PyQt5.QtGui import QColor, QBrush
@@ -18,8 +18,7 @@ from PyQt5.QtGui import QColor, QBrush
from data_model import ReceiverManager, GNSS_NAMES, GNSS_COLORS from data_model import ReceiverManager, GNSS_NAMES, GNSS_COLORS
from sky_widget import SkyPlotWidget from sky_widget import SkyPlotWidget
_COL_HEADERS = ['GNSS', 'SV', 'Elev °', 'Az °', 'C/N₀', 'Fix', 'Receiver', 'Trail'] _COL_HEADERS = ['GNSS', 'SV', 'Elev °', 'Az °', 'C/N₀', 'Fix', 'Receiver']
_COL_TRAIL = len(_COL_HEADERS) - 1
_TRAIL_MAXLEN = 3600 # 1 hour at 1 Hz _TRAIL_MAXLEN = 3600 # 1 hour at 1 Hz
@@ -27,29 +26,34 @@ class SkyTab(QWidget):
def __init__(self, model: ReceiverManager): def __init__(self, model: ReceiverManager):
super().__init__() super().__init__()
self._model = model self._model = model
self._store: dict = {} # (rid, gnss_id, sv_id) -> SatelliteData self._store: dict = {} # (rid, gnss_id, sv_id) -> SatelliteData
self._table_keys: list = [] # current row order as (rid, gnss_id, sv_id) self._table_keys: list = [] # current row order as (rid, gnss_id, sv_id)
self._pdop: dict = {} # rid -> float self._pdop: dict = {} # rid -> float
self._info_cards: dict = {} # rid -> QFrame self._info_cards: dict = {} # rid -> QFrame
self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used) self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used)
self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)] self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)]
self._trail_enabled: dict = {} # (rid, gnss_id, sv_id) -> bool
outer = QVBoxLayout(self) outer = QVBoxLayout(self)
outer.setContentsMargins(4, 4, 4, 4) outer.setContentsMargins(4, 4, 4, 4)
outer.setSpacing(4) outer.setSpacing(4)
# ── Source selector ─────────────────────────────────────────────────── # ── Source selector + trail toggle ────────────────────────────────────
src_box = QGroupBox("Receiver Sources (check to include)") src_box = QGroupBox("Receiver Sources (check to include)")
src_box.setMaximumHeight(80) src_box.setMaximumHeight(80)
src_bl = QHBoxLayout(src_box) src_bl = QHBoxLayout(src_box)
src_bl.setContentsMargins(4, 2, 4, 2) src_bl.setContentsMargins(4, 2, 4, 2)
src_bl.setSpacing(8)
self._src_list = QListWidget() self._src_list = QListWidget()
self._src_list.setFlow(QListWidget.LeftToRight) self._src_list.setFlow(QListWidget.LeftToRight)
self._src_list.setWrapping(True) self._src_list.setWrapping(True)
self._src_list.setResizeMode(QListWidget.Adjust) self._src_list.setResizeMode(QListWidget.Adjust)
self._src_list.itemChanged.connect(self._refresh_immediate) self._src_list.itemChanged.connect(self._refresh_immediate)
src_bl.addWidget(self._src_list) src_bl.addWidget(self._src_list)
self._trail_cb = QCheckBox("Show Trails")
self._trail_cb.setChecked(False)
self._trail_cb.toggled.connect(self._on_trail_toggled)
src_bl.addWidget(self._trail_cb)
outer.addWidget(src_box) outer.addWidget(src_box)
# ── Info bar (one card per receiver) ────────────────────────────────── # ── Info bar (one card per receiver) ──────────────────────────────────
@@ -73,16 +77,10 @@ class SkyTab(QWidget):
self._table.setHorizontalHeaderLabels(_COL_HEADERS) self._table.setHorizontalHeaderLabels(_COL_HEADERS)
hdr = self._table.horizontalHeader() hdr = self._table.horizontalHeader()
hdr.setSectionResizeMode(QHeaderView.ResizeToContents) hdr.setSectionResizeMode(QHeaderView.ResizeToContents)
hdr.setSectionResizeMode(_COL_TRAIL, QHeaderView.Fixed) hdr.setStretchLastSection(True)
hdr.setStretchLastSection(False)
hdr.sectionClicked.connect(self._on_header_clicked)
self._table.setColumnWidth(_COL_TRAIL, 42)
self._table.horizontalHeader().setToolTip(
"Click 'Trail' header to select / deselect all")
self._table.setEditTriggers(QTableWidget.NoEditTriggers) self._table.setEditTriggers(QTableWidget.NoEditTriggers)
self._table.setSelectionBehavior(QTableWidget.SelectRows) self._table.setSelectionBehavior(QTableWidget.SelectRows)
self._table.setAlternatingRowColors(False) self._table.setAlternatingRowColors(False)
self._table.itemChanged.connect(self._on_table_item_changed)
splitter.addWidget(self._table) splitter.addWidget(self._table)
splitter.setStretchFactor(0, 2) splitter.setStretchFactor(0, 2)
@@ -101,7 +99,7 @@ class SkyTab(QWidget):
model.satellite_update.connect(self._on_sat_update) model.satellite_update.connect(self._on_sat_update)
model.pdop_update.connect(self._on_pdop_update) model.pdop_update.connect(self._on_pdop_update)
# ── Source list management ─────────────────────────────────────────────── # ── Source list management ───────────────────────────────────────────────
def _on_receiver_added(self, rid: str): def _on_receiver_added(self, rid: str):
item = QListWidgetItem(f" {rid} ") item = QListWidgetItem(f" {rid} ")
@@ -130,9 +128,8 @@ class SkyTab(QWidget):
if self._src_list.item(i).data(Qt.UserRole) == rid: if self._src_list.item(i).data(Qt.UserRole) == rid:
self._src_list.takeItem(i) self._src_list.takeItem(i)
break break
for k in [k for k in self._store if k[0] == rid]: del self._store[k] for k in [k for k in self._store if k[0] == rid]: del self._store[k]
for k in [k for k in self._trail if k[0] == rid]: del self._trail[k] for k in [k for k in self._trail if k[0] == rid]: del self._trail[k]
for k in [k for k in self._trail_enabled if k[0] == rid]: del self._trail_enabled[k]
self._pdop.pop(rid, None) self._pdop.pop(rid, None)
card = self._info_cards.pop(rid, None) card = self._info_cards.pop(rid, None)
if card: if card:
@@ -155,12 +152,17 @@ class SkyTab(QWidget):
for k in self._store if k[0] in selected] for k in self._store if k[0] in selected]
def _active_trails(self) -> dict: def _active_trails(self) -> dict:
if not self._trail_cb.isChecked():
return {}
selected = self._selected_rids() selected = self._selected_rids()
result = {} return {key: list(dq)
for key, dq in self._trail.items(): for key, dq in self._trail.items()
if key[0] in selected and self._trail_enabled.get(key, False): if key[0] in selected}
result[key] = list(dq)
return result # ── Trail toggle ──────────────────────────────────────────────────────────
def _on_trail_toggled(self, _checked: bool):
self._sky.update_data(self._visible_sats(), self._active_trails())
# ── Info bar ────────────────────────────────────────────────────────────── # ── Info bar ──────────────────────────────────────────────────────────────
@@ -203,39 +205,6 @@ class SkyTab(QWidget):
def _rebuild_table_from_store(self): def _rebuild_table_from_store(self):
self._rebuild_table(self._visible_sats()) self._rebuild_table(self._visible_sats())
# ── Trail toggle ──────────────────────────────────────────────────────────
def _on_table_item_changed(self, item):
if item.column() != _COL_TRAIL:
return
key = item.data(Qt.UserRole)
if key is None:
return
self._trail_enabled[key] = (item.checkState() == Qt.Checked)
self._sky.update_data(self._visible_sats(), self._active_trails())
def _on_header_clicked(self, col: int):
if col != _COL_TRAIL:
return
# Enable all if any are off, otherwise disable all
rows = self._table.rowCount()
any_off = any(
self._table.item(r, _COL_TRAIL).checkState() != Qt.Checked
for r in range(rows)
if self._table.item(r, _COL_TRAIL)
)
new_state = Qt.Checked if any_off else Qt.Unchecked
self._table.blockSignals(True)
for r in range(rows):
item = self._table.item(r, _COL_TRAIL)
if item:
key = item.data(Qt.UserRole)
if key:
self._trail_enabled[key] = (new_state == Qt.Checked)
item.setCheckState(new_state)
self._table.blockSignals(False)
self._sky.update_data(self._visible_sats(), self._active_trails())
# ── Table ───────────────────────────────────────────────────────────────── # ── Table ─────────────────────────────────────────────────────────────────
def _rebuild_table(self, sats: list): def _rebuild_table(self, sats: list):
@@ -244,25 +213,21 @@ class SkyTab(QWidget):
if new_keys == self._table_keys: if new_keys == self._table_keys:
# Only values changed — update in place, scroll position untouched # Only values changed — update in place, scroll position untouched
self._table.blockSignals(True)
for row, (_, sat) in enumerate(sorted_sats): for row, (_, sat) in enumerate(sorted_sats):
self._table.item(row, 2).setText(f"{sat.elevation:.0f}") self._table.item(row, 2).setText(f"{sat.elevation:.0f}")
self._table.item(row, 3).setText(f"{sat.azimuth:.0f}") self._table.item(row, 3).setText(f"{sat.azimuth:.0f}")
self._table.item(row, 4).setText(f"{sat.cno:.1f}") self._table.item(row, 4).setText(f"{sat.cno:.1f}")
self._table.item(row, 5).setText("" if sat.used_in_fix else "") self._table.item(row, 5).setText("" if sat.used_in_fix else "")
self._table.blockSignals(False)
return return
# Satellite set changed — full rebuild # Satellite set changed — full rebuild
self._table_keys = new_keys self._table_keys = new_keys
self._table.blockSignals(True)
vp = self._table.viewport() vp = self._table.viewport()
vp.setUpdatesEnabled(False) vp.setUpdatesEnabled(False)
self._table.setSortingEnabled(False) self._table.setSortingEnabled(False)
self._table.setRowCount(len(sorted_sats)) self._table.setRowCount(len(sorted_sats))
for row, (rid, sat) in enumerate(sorted_sats): for row, (rid, sat) in enumerate(sorted_sats):
key = (rid, sat.gnss_id, sat.sv_id)
gnss_name = GNSS_NAMES.get(sat.gnss_id, f'SYS{sat.gnss_id}') gnss_name = GNSS_NAMES.get(sat.gnss_id, f'SYS{sat.gnss_id}')
bg = QColor(GNSS_COLORS.get(sat.gnss_id, '#9E9E9E')) bg = QColor(GNSS_COLORS.get(sat.gnss_id, '#9E9E9E'))
bg.setAlpha(55) bg.setAlpha(55)
@@ -282,14 +247,5 @@ class SkyTab(QWidget):
self._table.setItem(row, 5, cell("" if sat.used_in_fix else "")) self._table.setItem(row, 5, cell("" if sat.used_in_fix else ""))
self._table.setItem(row, 6, cell(rid)) self._table.setItem(row, 6, cell(rid))
trail_item = QTableWidgetItem()
trail_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
trail_item.setCheckState(
Qt.Checked if self._trail_enabled.get(key, False) else Qt.Unchecked)
trail_item.setData(Qt.UserRole, key)
trail_item.setTextAlignment(Qt.AlignCenter)
self._table.setItem(row, _COL_TRAIL, trail_item)
self._table.setSortingEnabled(True) self._table.setSortingEnabled(True)
vp.setUpdatesEnabled(True) vp.setUpdatesEnabled(True)
self._table.blockSignals(False)