f0f622b9ab
- One _apply_alignment_result() replaces the five copies of the cache-clear / generation-bump / Aligned-View-checkbox dance, and _on_manual_alignment_saved/_cleared collapse into a shared _manual_alignment_changed. - QSignalBlocker context managers replace every hand-rolled blockSignals(True)/set/blockSignals(False) triple (11 sites). - _make_dspin() replaces the 11 four-to-seven-line QDoubleSpinBox constructions; _axes_extent() replaces the duplicated imshow-extent formula; _is_fft_mode() replaces the repeated CH1-mode guard. - Jobs constants replace the stringly-typed background-job keys. - The two 160-line widget builders split along their section banners: _build_left_panel -> file/info/view/roi groups, the manual-alignment _build_ui -> six per-group builders + _connect_controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
152 lines
5.2 KiB
Python
152 lines
5.2 KiB
Python
"""Shared constants and small layout helpers for the viewer widgets."""
|
|
|
|
from PyQt6.QtCore import Qt
|
|
from PyQt6.QtWidgets import (
|
|
QDoubleSpinBox, QFormLayout, QFrame, QGroupBox, QLabel, QScrollArea,
|
|
QSizePolicy, QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from sras_format import CH1_IDX, CH3_IDX, CH4_IDX
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Display constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CH_LABELS = [
|
|
"CH1 — RF (FFT peak freq)",
|
|
"CH3 — Bias A (DC mean)",
|
|
"CH4 — Bias B (DC mean)",
|
|
"CH1 — Velocity (SRAS)",
|
|
]
|
|
|
|
# Combo index for the derived velocity mode (uses CH1_IDX data)
|
|
VELOCITY_MODE_IDX = 3
|
|
# All modes that operate on CH1 waveforms
|
|
CH1_DERIVED_MODES = (CH1_IDX, VELOCITY_MODE_IDX)
|
|
|
|
CMAPS = ["gray", "viridis", "plasma", "inferno", "hot", "jet", "RdBu_r", "seismic"]
|
|
|
|
# (mode_str, status-bar unit, colorbar label) per channel index
|
|
_CHANNEL_DISPLAY = {
|
|
CH1_IDX: ("RF", "Peak frequency (MHz)", "MHz"),
|
|
CH3_IDX: ("DC", "DC mean (mV)", "mV"),
|
|
CH4_IDX: ("DC", "DC mean (mV)", "mV"),
|
|
VELOCITY_MODE_IDX: ("Velocity", "Velocity (m/s)", "m/s"),
|
|
}
|
|
|
|
_CSS_HINT = "font-size: 11px; color: #aaa;"
|
|
_CSS_INFO = "font-size: 11px;"
|
|
_CSS_MUTED = "color: #888; font-size: 11px;"
|
|
_CSS_WARN = "color: #e07000; font-size: 11px;"
|
|
_CSS_BUSY = "color: #4a90d9; font-size: 11px;"
|
|
|
|
# Side-panel column widths (the scroll areas that hold the controls).
|
|
_LEFT_PANEL_W = 288
|
|
_RIGHT_PANEL_W = 272
|
|
|
|
# Minimum width for a spin box so its value + suffix are never clipped.
|
|
_SPIN_MIN_W = 96
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Small layout helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class Jobs:
|
|
"""Keys for SrasViewerWindow's background-job registry (_run_worker /
|
|
_job_running) and its progress dialogs — one place instead of string
|
|
literals scattered across window and dialogs."""
|
|
LOAD = "load"
|
|
COMPUTE = "compute"
|
|
DC_PRECOMPUTE = "dc_precompute"
|
|
BATCH = "batch"
|
|
ALIGN = "align"
|
|
MANUAL_ALIGN_MASKS = "manual_align_masks"
|
|
MANUAL_ALIGN_CORRELATE = "manual_align_correlate"
|
|
|
|
|
|
def _make_dspin(lo: float, hi: float, decimals: int, *, suffix: str = "",
|
|
value: float | None = None, step: float | None = None) -> QDoubleSpinBox:
|
|
"""A QDoubleSpinBox with the panel-standard construction."""
|
|
spin = QDoubleSpinBox()
|
|
spin.setRange(lo, hi)
|
|
spin.setDecimals(decimals)
|
|
if suffix:
|
|
spin.setSuffix(suffix)
|
|
if step is not None:
|
|
spin.setSingleStep(step)
|
|
if value is not None:
|
|
spin.setValue(value)
|
|
spin.setMinimumWidth(_SPIN_MIN_W)
|
|
return spin
|
|
|
|
|
|
def _axes_extent(x_axis, y_axis, dx: float, dy: float) -> list[float]:
|
|
"""Matplotlib imshow extent with half-pixel margins, Y flipped so row 0
|
|
renders at the top."""
|
|
return [x_axis[0] - dx / 2, x_axis[-1] + dx / 2,
|
|
y_axis[-1] + dy / 2, y_axis[0] - dy / 2]
|
|
|
|
|
|
def _wrap_label(text: str = "", css: str | None = None) -> QLabel:
|
|
"""A word-wrapped QLabel that reports its *wrapped* height to the layout.
|
|
|
|
A plain word-wrapped QLabel advertises a single-line minimum height, so in a
|
|
fixed-width column the layout happily shrinks it and the extra lines get
|
|
clipped. Enabling height-for-width makes the box layout ask for the real
|
|
height at the column's width instead.
|
|
"""
|
|
lbl = QLabel(text)
|
|
lbl.setWordWrap(True)
|
|
sp = lbl.sizePolicy()
|
|
sp.setVerticalPolicy(QSizePolicy.Policy.Minimum)
|
|
sp.setHeightForWidth(True)
|
|
lbl.setSizePolicy(sp)
|
|
if css:
|
|
lbl.setStyleSheet(css)
|
|
return lbl
|
|
|
|
|
|
def _group(title: str) -> tuple[QGroupBox, QVBoxLayout]:
|
|
"""A group box with consistent, non-cramped internal margins."""
|
|
grp = QGroupBox(title)
|
|
lay = QVBoxLayout(grp)
|
|
lay.setContentsMargins(10, 8, 10, 10)
|
|
lay.setSpacing(6)
|
|
return grp, lay
|
|
|
|
|
|
def _form() -> QFormLayout:
|
|
"""A label/field form layout for a narrow side panel."""
|
|
form = QFormLayout()
|
|
form.setContentsMargins(0, 0, 0, 0)
|
|
form.setHorizontalSpacing(8)
|
|
form.setVerticalSpacing(6)
|
|
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight
|
|
| Qt.AlignmentFlag.AlignVCenter)
|
|
form.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
|
form.setFieldGrowthPolicy(
|
|
QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
|
form.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows)
|
|
return form
|
|
|
|
|
|
def _scroll_panel(inner: QWidget, width: int) -> QScrollArea:
|
|
"""Put a side panel in a fixed-width scroll area.
|
|
|
|
Without this the panels are sized by the window: a short window squeezes the
|
|
controls past their minimum heights, which is what makes text overlap the
|
|
widget below it. Scrolling keeps every control at its natural size.
|
|
"""
|
|
area = QScrollArea()
|
|
area.setWidget(inner)
|
|
area.setWidgetResizable(True)
|
|
area.setFrameShape(QFrame.Shape.NoFrame)
|
|
area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
area.setFixedWidth(width)
|
|
area.viewport().setAutoFillBackground(False)
|
|
inner.setAutoFillBackground(False)
|
|
return area
|
|
|