"""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" # The alignment wizard's three background steps: fetching each angle's CH4 # image for the mask stack, registering the angles, and writing the aligned # export. Separate keys because a retry of one must not be blocked by # another having run, and _run_worker's busy check is per key. ALIGN_MASKS = "align_masks" ALIGN_CORRELATE = "align_correlate" ALIGN_EXPORT = "align_export" 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