Dedup viewer Qt boilerplate after the split

- 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>
This commit is contained in:
Thomas Ales
2026-08-06 10:56:50 -05:00
parent 26a34f7436
commit f0f622b9ab
3 changed files with 172 additions and 186 deletions
+49 -78
View File
@@ -5,11 +5,11 @@ from typing import TYPE_CHECKING
import matplotlib as mpl
import numpy as np
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtCore import QSignalBlocker, pyqtSignal
from PyQt6.QtWidgets import (
QButtonGroup, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox,
QGroupBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QRadioButton,
QSpinBox, QVBoxLayout, QWidget,
QButtonGroup, QComboBox, QDialog, QDialogButtonBox, QGroupBox,
QHBoxLayout, QLabel, QMessageBox, QPushButton, QRadioButton, QSpinBox,
QVBoxLayout, QWidget,
)
import sras_compute as compute
@@ -22,7 +22,7 @@ from sras_workers import Ch4MaskWorker, CrossCorrelateWorker
from .canvases import ManualAlignOverlayCanvas
from .common import (
_CSS_HINT, _CSS_MUTED, _CSS_WARN, _SPIN_MIN_W, _form, _group,
_CSS_HINT, _CSS_MUTED, _CSS_WARN, Jobs, _axes_extent, _form, _group, _make_dspin,
_scroll_panel, _wrap_label,
)
@@ -268,8 +268,20 @@ class ManualAlignmentDialog(QDialog):
panel_l = QVBoxLayout(panel)
panel_l.setContentsMargins(0, 0, 0, 0)
panel_l.setSpacing(8)
panel_l.addWidget(self._build_angle_group())
panel_l.addWidget(self._build_adjust_group())
panel_l.addWidget(self._build_step_group())
panel_l.addWidget(self._build_threshold_group(dc_threshold_mv))
panel_l.addWidget(self._build_correlate_group())
panel_l.addWidget(self._build_actions_group())
self.lbl_status = _wrap_label("", _CSS_MUTED)
panel_l.addWidget(self.lbl_status)
panel_l.addStretch()
# ---- Active Angle -------------------------------------------------
root.addWidget(_scroll_panel(panel, 320))
self._connect_controls()
def _build_angle_group(self) -> QWidget:
grp_angle, al = _group("Active Angle")
self.combo_active_angle = QComboBox()
for a in range(self._sras.n_angles):
@@ -280,80 +292,52 @@ class ManualAlignmentDialog(QDialog):
al.addWidget(self.combo_active_angle)
self.lbl_active_note = _wrap_label("", _CSS_WARN)
al.addWidget(self.lbl_active_note)
panel_l.addWidget(grp_angle)
return grp_angle
# ---- Manual Adjustment ---------------------------------------------
def _build_adjust_group(self) -> QWidget:
self.grp_manual_adjust, mform_box = _group("Manual Adjustment")
mform = _form()
self.spin_active_rotation_deg = QDoubleSpinBox()
self.spin_active_rotation_deg.setRange(-3600.0, 3600.0)
self.spin_active_rotation_deg.setDecimals(3)
self.spin_active_rotation_deg.setSuffix(" °")
self.spin_active_rotation_deg.setMinimumWidth(_SPIN_MIN_W)
self.spin_active_rotation_deg = _make_dspin(-3600.0, 3600.0, 3, suffix=" °")
mform.addRow("Rotation:", self.spin_active_rotation_deg)
self.spin_active_shift_x_mm = QDoubleSpinBox()
self.spin_active_shift_x_mm.setRange(-1e5, 1e5)
self.spin_active_shift_x_mm.setDecimals(4)
self.spin_active_shift_x_mm.setSuffix(" mm")
self.spin_active_shift_x_mm.setMinimumWidth(_SPIN_MIN_W)
self.spin_active_shift_x_mm = _make_dspin(-1e5, 1e5, 4, suffix=" mm")
mform.addRow("Shift X:", self.spin_active_shift_x_mm)
self.spin_active_shift_y_mm = QDoubleSpinBox()
self.spin_active_shift_y_mm.setRange(-1e5, 1e5)
self.spin_active_shift_y_mm.setDecimals(4)
self.spin_active_shift_y_mm.setSuffix(" mm")
self.spin_active_shift_y_mm.setMinimumWidth(_SPIN_MIN_W)
self.spin_active_shift_y_mm = _make_dspin(-1e5, 1e5, 4, suffix=" mm")
mform.addRow("Shift Y:", self.spin_active_shift_y_mm)
mform_box.addLayout(mform)
panel_l.addWidget(self.grp_manual_adjust)
return self.grp_manual_adjust
# ---- Nudge Step Sizes ------------------------------------------------
def _build_step_group(self) -> QWidget:
self.grp_step_sizes, sl = _group("Nudge Step Sizes")
sform = _form()
self.spin_step_translate_mm = QDoubleSpinBox()
self.spin_step_translate_mm.setRange(0.0001, 1000.0)
self.spin_step_translate_mm.setDecimals(4)
self.spin_step_translate_mm.setSuffix(" mm")
self.spin_step_translate_mm.setValue(0.01)
self.spin_step_translate_mm.setMinimumWidth(_SPIN_MIN_W)
self.spin_step_translate_mm = _make_dspin(0.0001, 1000.0, 4,
suffix=" mm", value=0.01)
sform.addRow("Translate step:", self.spin_step_translate_mm)
self.spin_step_rotate_deg = QDoubleSpinBox()
self.spin_step_rotate_deg.setRange(0.001, 90.0)
self.spin_step_rotate_deg.setDecimals(3)
self.spin_step_rotate_deg.setSuffix(" °")
self.spin_step_rotate_deg.setValue(0.1)
self.spin_step_rotate_deg.setMinimumWidth(_SPIN_MIN_W)
self.spin_step_rotate_deg = _make_dspin(0.001, 90.0, 3,
suffix=" °", value=0.1)
sform.addRow("Rotate step:", self.spin_step_rotate_deg)
self.spin_step_multiplier = QDoubleSpinBox()
self.spin_step_multiplier.setRange(1.0, 1000.0)
self.spin_step_multiplier.setDecimals(1)
self.spin_step_multiplier.setValue(10.0)
self.spin_step_multiplier.setMinimumWidth(_SPIN_MIN_W)
self.spin_step_multiplier = _make_dspin(1.0, 1000.0, 1, value=10.0)
sform.addRow("Coarse × (Shift):", self.spin_step_multiplier)
sl.addLayout(sform)
sl.addWidget(_wrap_label(
"Arrow keys nudge X/Y translation; Q/E nudge rotation (CCW/CW). "
"Hold Shift for the coarse step. Click the image once so it has "
"keyboard focus.", _CSS_HINT))
panel_l.addWidget(self.grp_step_sizes)
return self.grp_step_sizes
# ---- Mask Threshold ---------------------------------------------------
def _build_threshold_group(self, dc_threshold_mv: float) -> QWidget:
self.grp_mask_threshold, tl = _group("Mask Threshold")
tform = _form()
self.spin_mask_threshold_mv = QDoubleSpinBox()
self.spin_mask_threshold_mv.setRange(-500.0, 500.0)
self.spin_mask_threshold_mv.setDecimals(3)
self.spin_mask_threshold_mv.setSuffix(" mV")
self.spin_mask_threshold_mv.setValue(dc_threshold_mv)
self.spin_mask_threshold_mv.setMinimumWidth(_SPIN_MIN_W)
self.spin_mask_threshold_mv = _make_dspin(-500.0, 500.0, 3,
suffix=" mV", value=dc_threshold_mv)
tform.addRow("DC threshold:", self.spin_mask_threshold_mv)
tl.addLayout(tform)
panel_l.addWidget(self.grp_mask_threshold)
return self.grp_mask_threshold
# ---- Cross-Correlate (FFT) -----------------------------------------
def _build_correlate_group(self) -> QWidget:
self.grp_correlate, cl = _group("Cross-Correlate (FFT)")
cform = _form()
self.combo_correlate_source = QComboBox()
@@ -361,13 +345,8 @@ class ManualAlignmentDialog(QDialog):
self.combo_correlate_source.addItem(label, sources)
cform.addRow("Correlate on:", self.combo_correlate_source)
self.spin_correlate_search_deg = QDoubleSpinBox()
self.spin_correlate_search_deg.setRange(0.0, 180.0)
self.spin_correlate_search_deg.setSingleStep(1.0)
self.spin_correlate_search_deg.setDecimals(1)
self.spin_correlate_search_deg.setSuffix(" °")
self.spin_correlate_search_deg.setValue(6.0)
self.spin_correlate_search_deg.setMinimumWidth(_SPIN_MIN_W)
self.spin_correlate_search_deg = _make_dspin(0.0, 180.0, 1, suffix=" °",
value=6.0, step=1.0)
cform.addRow("Rotation search (±):", self.spin_correlate_search_deg)
cl.addLayout(cform)
self.btn_auto_correlate = QPushButton("Auto Cross-Correlate (vs Reference)")
@@ -378,9 +357,9 @@ class ManualAlignmentDialog(QDialog):
"reported angle is only the starting point of the search, and both "
"of its signs are tried. Run this first, then nudge only for small "
"corrections.", _CSS_HINT))
panel_l.addWidget(self.grp_correlate)
return self.grp_correlate
# ---- Actions ------------------------------------------------------
def _build_actions_group(self) -> QWidget:
grp_actions, acl = _group("Actions")
self.btn_auto_derotate = QPushButton("Auto De-rotate (use known angles)")
self.btn_save = QPushButton("Save Alignment")
@@ -388,14 +367,9 @@ class ManualAlignmentDialog(QDialog):
self.btn_close = QPushButton("Close")
for btn in (self.btn_auto_derotate, self.btn_save, self.btn_clear, self.btn_close):
acl.addWidget(btn)
panel_l.addWidget(grp_actions)
self.lbl_status = _wrap_label("", _CSS_MUTED)
panel_l.addWidget(self.lbl_status)
panel_l.addStretch()
root.addWidget(_scroll_panel(panel, 320))
return grp_actions
def _connect_controls(self):
self.combo_active_angle.currentIndexChanged.connect(self._on_active_angle_changed)
self.spin_active_rotation_deg.editingFinished.connect(self._on_rotation_spin_edited)
self.spin_active_shift_x_mm.editingFinished.connect(self._on_shift_spin_edited)
@@ -409,9 +383,8 @@ class ManualAlignmentDialog(QDialog):
self.canvas.nudge_translate.connect(self._on_nudge_translate)
self.canvas.nudge_rotate.connect(self._on_nudge_rotate)
self.combo_active_angle.blockSignals(True)
self.combo_active_angle.setCurrentIndex(self._active_angle)
self.combo_active_angle.blockSignals(False)
with QSignalBlocker(self.combo_active_angle):
self.combo_active_angle.setCurrentIndex(self._active_angle)
self._on_active_angle_changed(self._active_angle)
# ------------------------------------------------------------------
@@ -426,7 +399,7 @@ class ManualAlignmentDialog(QDialog):
return
self.lbl_status.setText(f"Preparing masks: 0/{len(missing)} angle(s) needed…")
started = self._parent._run_worker(
"manual_align_masks", Ch4MaskWorker(self._sras, missing),
Jobs.MANUAL_ALIGN_MASKS, Ch4MaskWorker(self._sras, missing),
connect=(
("angle_done", self._on_mask_angle_done),
("error", lambda msg: self.lbl_status.setText(f"Mask prep error: {msg}")),
@@ -541,8 +514,7 @@ class ManualAlignmentDialog(QDialog):
dx, dy = self._preview_pitch_mm
x_axis = x0 + np.arange(n_cols) * dx
y_axis = y0 + np.arange(n_rows) * dy
extent = [x_axis[0] - dx / 2, x_axis[-1] + dx / 2,
y_axis[-1] + dy / 2, y_axis[0] - dy / 2]
extent = _axes_extent(x_axis, y_axis, dx, dy)
title = (f"Angle {self._active_angle} active "
f"({self._sras.angles_deg[self._active_angle]:.1f}°)")
self.canvas.show_overlay(rgba, extent, title)
@@ -565,9 +537,8 @@ class ManualAlignmentDialog(QDialog):
for spin, val in ((self.spin_active_rotation_deg, p.rotation_deg),
(self.spin_active_shift_x_mm, p.shift_mm[0]),
(self.spin_active_shift_y_mm, p.shift_mm[1])):
spin.blockSignals(True)
spin.setValue(val)
spin.blockSignals(False)
with QSignalBlocker(spin):
spin.setValue(val)
def _on_nudge_translate(self, dir_x: int, dir_y: int, coarse: bool):
if not self._masks_ready or self._active_angle == self._ref_angle_idx:
@@ -655,7 +626,7 @@ class ManualAlignmentDialog(QDialog):
self._set_controls_enabled(False)
self.lbl_status.setText(f"Cross-correlating: 0/{self._correlate_total} angle(s)…")
started = self._parent._run_worker(
"manual_align_correlate", worker,
Jobs.MANUAL_ALIGN_CORRELATE, worker,
connect=(
("angle_done", self._on_correlate_angle_done),
("error", self._on_correlate_error),