From 623f803ceb8d9044238bd325a9d01e87d03f1dfd Mon Sep 17 00:00:00 2001 From: Dawn Date: Thu, 20 Aug 2026 16:21:21 +0200 Subject: [PATCH] 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 --- .../gui/panels/data_collection_settings.py | 56 ++++++++++++++---- .../unit/gui/test_data_collection_settings.py | 59 ++++++++++++++++++- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/src/aare/gui/panels/data_collection_settings.py b/src/aare/gui/panels/data_collection_settings.py index 356e2d2d..834d7e35 100644 --- a/src/aare/gui/panels/data_collection_settings.py +++ b/src/aare/gui/panels/data_collection_settings.py @@ -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: diff --git a/tests/unit/gui/test_data_collection_settings.py b/tests/unit/gui/test_data_collection_settings.py index a9924864..565e9402 100644 --- a/tests/unit/gui/test_data_collection_settings.py +++ b/tests/unit/gui/test_data_collection_settings.py @@ -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