feat: auto-centering row with apply-after-mount, grid button raster-only
Auto Centering moves out of the experiment tabs to its own row between Manual sample and Exp. Config., with an apply-after-mount checkbox that re-runs it on every detected sample mount (guarded against firing on GUI startup). Draw a Grid now only shows on the Raster scan tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ from aarecommon.math.sample_geometry import SampleGeometryModel
|
||||
from aarecommon.models.models import DAQStatusModel
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDoubleSpinBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -55,9 +56,24 @@ class DataCollectionSettings(QFrame):
|
||||
self.manual_sample_panel = ManualSamplePanel(self)
|
||||
v_layout.addWidget(self.manual_sample_panel)
|
||||
|
||||
# QTabBar + QStackedWidget instead of QTabWidget: the loop-centering
|
||||
# button row must sit BETWEEN the tab bar and the pages, which a
|
||||
# QTabWidget cannot host.
|
||||
# Auto Centering applies to every scan type, so it lives outside the
|
||||
# experiment tabs, between Manual sample and Exp. Config. The checkbox
|
||||
# arms an automatic run after each sample mount (see update_daq_status).
|
||||
self.find_tip = QPushButton("Auto Centering", parent=self)
|
||||
self.auto_center_after_mount = QCheckBox("apply after mount", parent=self)
|
||||
self.auto_center_after_mount.setToolTip(
|
||||
"Run Auto Centering automatically after each sample mount"
|
||||
)
|
||||
auto_center_row = QWidget(self)
|
||||
auto_center_layout = QHBoxLayout(auto_center_row)
|
||||
auto_center_layout.setContentsMargins(0, 0, 0, 0)
|
||||
auto_center_layout.addWidget(self.find_tip, 1)
|
||||
auto_center_layout.addWidget(self.auto_center_after_mount)
|
||||
v_layout.addWidget(auto_center_row)
|
||||
|
||||
# QTabBar + QStackedWidget instead of QTabWidget: the grid button
|
||||
# must sit BETWEEN the tab bar and the pages, which a QTabWidget
|
||||
# cannot host.
|
||||
self._tab_bar = QTabBar(self)
|
||||
self._stack = QStackedWidget(self)
|
||||
|
||||
@@ -76,14 +92,9 @@ class DataCollectionSettings(QFrame):
|
||||
self._stack.addWidget(panel)
|
||||
self._tab_bar.addTab(label)
|
||||
|
||||
# Ex-"Loop centering" panel buttons; always visible, whatever the tab.
|
||||
self.find_tip = QPushButton("Auto Centering", parent=self)
|
||||
# Only meaningful for raster scans; hidden on the other tabs
|
||||
# (_on_tab_changed).
|
||||
self.bounding_box = QPushButton("Draw a Grid", parent=self)
|
||||
centering_row = QWidget(self)
|
||||
centering_layout = QHBoxLayout(centering_row)
|
||||
centering_layout.setContentsMargins(0, 0, 0, 0)
|
||||
centering_layout.addWidget(self.find_tip)
|
||||
centering_layout.addWidget(self.bounding_box)
|
||||
|
||||
# Live readout mirrored from the Beamline setup panel, same reason:
|
||||
# "what is" and "what to set" must not share one ambiguous row.
|
||||
@@ -120,7 +131,7 @@ class DataCollectionSettings(QFrame):
|
||||
# No bottom padding: the pages' own bottom margins breathe inside the
|
||||
# border, and the Abort button should hug the pane.
|
||||
pane_layout.setContentsMargins(6, 6, 6, 0)
|
||||
pane_layout.addWidget(centering_row)
|
||||
pane_layout.addWidget(self.bounding_box)
|
||||
pane_layout.addWidget(current_energy_row)
|
||||
pane_layout.addWidget(energy_row)
|
||||
pane_layout.addWidget(self._stack)
|
||||
@@ -163,6 +174,10 @@ class DataCollectionSettings(QFrame):
|
||||
self.file_path_panel.path_updated.connect(self.screening.update_filename)
|
||||
self.file_path_panel.path_updated.connect(self.simple.update_filename)
|
||||
self._sample_id = None
|
||||
# False until the first DAQ status: a sample already mounted at GUI
|
||||
# startup must not trigger an auto-centering (hardware would move
|
||||
# uninvited on every restart).
|
||||
self._status_seen = False
|
||||
|
||||
self._tab_bar.currentChanged.connect(self._stack.setCurrentIndex)
|
||||
self._tab_bar.currentChanged.connect(self._on_tab_changed)
|
||||
@@ -208,12 +223,27 @@ class DataCollectionSettings(QFrame):
|
||||
self.raster.update_daq_status(s)
|
||||
self.screening.update_daq_status(s)
|
||||
self.simple.update_daq_status(s)
|
||||
if s.sample is not None and s.sample.db_id != self._sample_id:
|
||||
self._sample_id = s.sample.db_id
|
||||
self._track_sample(None if s.sample is None else s.sample.db_id)
|
||||
|
||||
def _track_sample(self, db_id: int | None):
|
||||
# Separate from update_daq_status so tests can drive mount detection
|
||||
# without building a full DAQStatusModel.
|
||||
if db_id is None:
|
||||
# Forget on unmount so remounting the same sample counts as a
|
||||
# fresh mount below.
|
||||
self._sample_id = None
|
||||
elif db_id != self._sample_id:
|
||||
self._sample_id = db_id
|
||||
self._tab_bar.setCurrentIndex(0)
|
||||
if self._status_seen and self.auto_center_after_mount.isChecked():
|
||||
# Reuse the button wiring (clicked -> daq.center_loop in
|
||||
# main_window) instead of a second signal path.
|
||||
self.find_tip.click()
|
||||
self._status_seen = True
|
||||
|
||||
@Slot(int)
|
||||
def _on_tab_changed(self, idx: int):
|
||||
self.bounding_box.setVisible(idx == 0)
|
||||
# 0: Raster, 1: Rotation (incl. screening), 2: Simple (rotation wrapper), 3: XRF (ignore)
|
||||
kind = "rotation"
|
||||
if idx == 0:
|
||||
|
||||
@@ -12,6 +12,7 @@ from aarecommon.math.coordinate import Coordinate, SmargonCoordinate
|
||||
from aarecommon.math.diffraction_geometry import DiffractionGeometry
|
||||
from aarecommon.models.models import SampleGeometryModel
|
||||
|
||||
from aare.gui.panels.data_collection_settings import DataCollectionSettings
|
||||
from aare.gui.panels.raster_data_collection import RasterDataCollectionPanel
|
||||
from aare.gui.panels.rotation_data_collection import RotationDataCollectionPanel
|
||||
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
@@ -171,8 +172,8 @@ def test_user_override_persists_across_samples(panel, diffraction):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def raster_panel(qapp, diffraction):
|
||||
geom = SampleGeometryModel(
|
||||
def geom():
|
||||
return SampleGeometryModel(
|
||||
beam_location_pxl=Coordinate(x=500, y=500),
|
||||
pixel_in_mm=0.001,
|
||||
aerotech=Coordinate(x=0, y=0, z=0),
|
||||
@@ -181,6 +182,10 @@ def raster_panel(qapp, diffraction):
|
||||
omega_deg=0.0,
|
||||
beam_size_mm=Coordinate(x=0.01, y=0.01),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def raster_panel(qapp, diffraction, geom):
|
||||
mgr = RasterGridManager(geom)
|
||||
return RasterDataCollectionPanel(raster_mgr=mgr, diffraction=diffraction)
|
||||
|
||||
@@ -212,3 +217,53 @@ def test_grid_size_field_shares_panel_toggle(raster_panel):
|
||||
_edit(raster_panel.high_res_enter, "2.50")
|
||||
assert raster_panel._source == DbOverrideLineEdit.SOURCE_MINE
|
||||
assert raster_panel.width_enter.source() == DbOverrideLineEdit.SOURCE_MINE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DataCollectionSettings: per-tab grid button + auto-center after mount
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings_panel(qapp, diffraction, geom):
|
||||
return DataCollectionSettings(geom, RasterGridManager(geom), diffraction)
|
||||
|
||||
|
||||
def test_grid_button_only_on_raster_tab(settings_panel):
|
||||
assert not settings_panel.bounding_box.isHidden() # Raster is the default tab
|
||||
for idx in (1, 2, 3):
|
||||
settings_panel._tab_bar.setCurrentIndex(idx)
|
||||
assert settings_panel.bounding_box.isHidden()
|
||||
settings_panel._tab_bar.setCurrentIndex(0)
|
||||
assert not settings_panel.bounding_box.isHidden()
|
||||
|
||||
|
||||
def test_auto_center_fires_on_mount_only_when_armed(settings_panel):
|
||||
clicks = []
|
||||
settings_panel.find_tip.clicked.connect(lambda: clicks.append(1))
|
||||
settings_panel.auto_center_after_mount.setChecked(True)
|
||||
|
||||
# First status with a sample already mounted = GUI (re)start: hardware
|
||||
# must not move, only the id is recorded.
|
||||
settings_panel._track_sample(1)
|
||||
assert not clicks
|
||||
|
||||
# Real mount after an unmount fires the centering.
|
||||
settings_panel._track_sample(None)
|
||||
settings_panel._track_sample(2)
|
||||
assert len(clicks) == 1
|
||||
|
||||
# Direct sample exchange (no unmount tick in between) also fires.
|
||||
settings_panel._track_sample(3)
|
||||
assert len(clicks) == 2
|
||||
|
||||
# Remounting the SAME sample counts as a fresh mount.
|
||||
settings_panel._track_sample(None)
|
||||
settings_panel._track_sample(3)
|
||||
assert len(clicks) == 3
|
||||
|
||||
# Disarmed: mounts no longer trigger.
|
||||
settings_panel.auto_center_after_mount.setChecked(False)
|
||||
settings_panel._track_sample(None)
|
||||
settings_panel._track_sample(4)
|
||||
assert len(clicks) == 3
|
||||
|
||||
Reference in New Issue
Block a user