diff --git a/src/aare/gui/panels/abr_tweak_panel.py b/src/aare/gui/panels/abr_tweak_panel.py
index f9cd172d..14b68100 100644
--- a/src/aare/gui/panels/abr_tweak_panel.py
+++ b/src/aare/gui/panels/abr_tweak_panel.py
@@ -92,32 +92,38 @@ class AbrTweakWidget(QWidget):
super().__init__(parent)
grid_layout = QGridLayout(self)
- grid_layout.setColumnStretch(0, 0)
- grid_layout.setColumnStretch(1, 0)
- grid_layout.setColumnStretch(2, 1)
- # Span all 4 grid columns (the ABR buttons row uses 4), otherwise the
- # banner renders narrower than the neighboring panels.
- grid_layout.addWidget(TitleLabel("ABR meas. pos.", self, collapsible=True), 0, 0, 1, 4)
+ grid_layout.addWidget(TitleLabel("ABR meas. pos.", self, collapsible=True, default_collapsed=False), 0, 0, 1, 2)
self._abr_buttons = AbrTweakButtons(DEFAULT_ABR_STEP_UM / 1000, parent=self)
- grid_layout.addWidget(self._abr_buttons, 1, 0, 1, 4)
+ grid_layout.addWidget(self._abr_buttons, 1, 0)
self._abr_buttons.abr_tweak.connect(self.abr_button_pressed)
- grid_layout.addWidget(QLabel("Step", parent=self), 2, 0)
- self._step_um = NumberLineEdit(1, 1000, DEFAULT_ABR_STEP_UM, 0, parent=self)
- grid_layout.addWidget(self._step_um, 2, 1)
- grid_layout.addWidget(QLabel("μm", parent=self), 2, 2)
+ # Step + actions as a column BESIDE the GM rows instead of below —
+ # the rows left plenty of dead width.
+ side = QWidget(self)
+ side_grid = QGridLayout(side)
+ side_grid.setContentsMargins(0, 0, 0, 0)
+ side_grid.addWidget(QLabel("Step", parent=side), 0, 0)
+ self._step_um = NumberLineEdit(1, 1000, DEFAULT_ABR_STEP_UM, 0, parent=side)
+ side_grid.addWidget(self._step_um, 0, 1)
+ side_grid.addWidget(QLabel("μm", parent=side), 0, 2)
self._step_um.newValue.connect(self._abr_buttons.set_step)
save_button = QPushButton("Save ABR pos.")
- grid_layout.addWidget(save_button, 3, 0, 1, 3)
+ side_grid.addWidget(save_button, 1, 0, 1, 3)
save_button.pressed.connect(self.save_button_pressed)
goto_button = QPushButton("Go to meas.")
- grid_layout.addWidget(goto_button, 4, 0, 1, 3)
+ side_grid.addWidget(goto_button, 2, 0, 1, 3)
goto_button.pressed.connect(self.goto_button_pressed)
+ # ~80% of the width the grid handed it; the freed space goes to the
+ # GM rows (column 0 takes all stretch).
+ side.setMaximumWidth(150)
+ grid_layout.setColumnStretch(0, 1)
+ grid_layout.addWidget(side, 1, 1)
+
@Slot()
def goto_button_pressed(self):
self.abr_goto_meas.emit()
diff --git a/src/aare/gui/panels/automation_panel.py b/src/aare/gui/panels/automation_panel.py
index 3cd4496b..1a1c3181 100644
--- a/src/aare/gui/panels/automation_panel.py
+++ b/src/aare/gui/panels/automation_panel.py
@@ -1,18 +1,16 @@
from __future__ import annotations
-import copy
import time
from datetime import datetime
from aarecommon.config.logger import setup_logger
from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind
-from PySide6.QtCore import QTimer, Slot
-from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
+from PySide6.QtCore import Qt, QTimer, Slot
+from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from aare.gui.constants import LOGGER_NAME
from aare.gui.styles import (
AUTOMATION_HINT_TEXT,
- AUTOMATION_TITLE_TEXT,
CARD_BORDER,
FAINT_TEXT,
FONT_BODY,
@@ -49,7 +47,6 @@ class AutomationProgressWidget(QWidget):
self._progress: AutomationProgress | None = None
self._gui_samples_in_queue: int | None = None
self._labels: dict[WorkflowStateKind, QLabel] = {}
- self._title_label: QLabel | None = None
self._stats_label: QLabel | None = None
self._is_paused = False
self._refresh_timer = QTimer(self)
@@ -59,36 +56,45 @@ class AutomationProgressWidget(QWidget):
self.clear()
def _setup_ui(self) -> None:
+ # No title label: the dock's title bar already says it.
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
- self._title_label = QLabel("Automation progress")
- self._title_label.setStyleSheet(
- f"font-size: {FONT_TITLE}; font-weight: 700; color: {AUTOMATION_TITLE_TEXT}; margin-bottom: 2px;"
- )
- layout.addWidget(self._title_label)
-
self._stats_label = QLabel()
self._stats_label.setStyleSheet(
f"color: {AUTOMATION_HINT_TEXT}; font-size: {FONT_LABEL}; font-weight: 700; "
f"background-color: {SURFACE}; border: 1px solid {CARD_BORDER}; "
- "border-radius: 8px; padding: 10px;"
+ "padding: 10px;"
)
self._stats_label.setWordWrap(True)
- layout.addWidget(self._stats_label)
+ # Stats left, automation run-state card right ("||" paused / "▶"
+ # running — ASCII bars: fancier pause glyphs are missing from the
+ # beamline console fonts).
+ self._state_label = QLabel()
+ self._state_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+
+ top_row = QHBoxLayout()
+ top_row.setSpacing(10)
+ top_row.addWidget(self._stats_label, 2)
+ top_row.addWidget(self._state_label, 1)
+ layout.addLayout(top_row)
+ self._update_state_label()
+
+ steps_row = QHBoxLayout()
+ steps_row.setSpacing(6)
for step in (
WorkflowStateKind.MOUNT,
WorkflowStateKind.LOOP_CENTRE,
WorkflowStateKind.RASTER,
WorkflowStateKind.DATA_COLLECTION,
- WorkflowStateKind.FINAL,
):
label = QLabel()
label.setWordWrap(True)
- layout.addWidget(label)
+ steps_row.addWidget(label, 1)
self._labels[step] = label
+ layout.addLayout(steps_row)
layout.addStretch()
@@ -142,7 +148,7 @@ class AutomationProgressWidget(QWidget):
@staticmethod
def _style_for_status(status: StepStatus) -> str:
base = (
- "padding: 10px 12px; border-radius: 10px; "
+ "padding: 10px 12px; "
f"font-size: {FONT_BODY}; border: 1px solid transparent;"
)
@@ -219,32 +225,38 @@ class AutomationProgressWidget(QWidget):
return
self.set_progress(self._progress)
+ def _update_state_label(self) -> None:
+ if self._is_paused:
+ colors = (
+ f"background-color: {STEP_PAUSED_BG}; color: {STEP_PAUSED_TEXT}; "
+ f"border: 1px solid {STEP_PAUSED_BORDER};"
+ )
+ text = "|| paused"
+ else:
+ colors = (
+ f"background-color: {STEP_ACTIVE_BG}; color: {STEP_ACTIVE_TEXT}; "
+ f"border: 1px solid {STEP_ACTIVE_BORDER};"
+ )
+ text = "▶ running"
+ self._state_label.setText(text)
+ self._state_label.setStyleSheet(
+ f"font-size: {FONT_TITLE}; font-weight: 700; padding: 10px; " + colors
+ )
+
@Slot(bool)
def set_running(self, running: bool) -> None:
self._is_paused = not running
+ self._update_state_label()
if self._progress is None:
return
- progress = copy.deepcopy(self._progress)
-
- final_step = next(
- (step for step in progress.steps if step.step == WorkflowStateKind.FINAL), None
- )
-
if self._is_paused:
self._refresh_timer.stop()
- if final_step is not None:
- final_step.status = StepStatus.PAUSED
- final_step.message = "Automation paused"
- else:
- if final_step is not None and final_step.status == StepStatus.PAUSED:
- final_step.status = StepStatus.PENDING
- final_step.message = ""
- if self._has_live_timing(progress):
- self._refresh_timer.start()
+ elif self._has_live_timing(self._progress):
+ self._refresh_timer.start()
- self.set_progress(progress)
+ self.set_progress(self._progress)
@Slot(int)
def set_samples_in_queue(self, count: int) -> None:
@@ -318,5 +330,3 @@ class AutomationProgressWidget(QWidget):
label.setText(f"{icon} {title}{duration_str}{message}{error_str}")
label.setStyleSheet(self._style_for_status(step_state.status))
- if self._title_label is not None:
- self._title_label.setText("Automation progress")
diff --git a/src/aare/gui/panels/beam_mark_panel.py b/src/aare/gui/panels/beam_mark_panel.py
index 3a74895a..5122ab78 100644
--- a/src/aare/gui/panels/beam_mark_panel.py
+++ b/src/aare/gui/panels/beam_mark_panel.py
@@ -12,8 +12,9 @@ class BeamMarkWidget(QWidget):
super().__init__(parent)
grid_layout = QGridLayout(self)
+ grid_layout.setVerticalSpacing(2)
- grid_layout.addWidget(section_title("Beam mark (image)", self), 0, 0, 1, 5)
+ grid_layout.addWidget(section_title("Beam mark (image)", self), 0, 0, 1, 6)
self.x = QLabel("0")
self.y = QLabel("0")
@@ -24,8 +25,10 @@ class BeamMarkWidget(QWidget):
grid_layout.addWidget(self.y, 1, 3)
grid_layout.addWidget(QLabel("pxl"), 1, 4)
+ # Shares the readings row instead of a full-width row below.
clear_button = QPushButton("Clear marks")
- grid_layout.addWidget(clear_button, 2, 0, 1, 5)
+ clear_button.setFixedWidth(90)
+ grid_layout.addWidget(clear_button, 1, 5)
clear_button.pressed.connect(self.clear_button_pressed)
@Slot()
diff --git a/src/aare/gui/panels/data_collection_settings.py b/src/aare/gui/panels/data_collection_settings.py
index df75e340..71ea0e9b 100644
--- a/src/aare/gui/panels/data_collection_settings.py
+++ b/src/aare/gui/panels/data_collection_settings.py
@@ -2,7 +2,16 @@ from aarecommon.math.diffraction_geometry import DiffractionGeometry
from aarecommon.math.sample_geometry import SampleGeometryModel
from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import Signal, Slot
-from PySide6.QtWidgets import QFrame, QPushButton, QTabWidget, QVBoxLayout, QWidget
+from PySide6.QtWidgets import (
+ QFrame,
+ QHBoxLayout,
+ QPushButton,
+ QSizePolicy,
+ QStackedWidget,
+ QTabBar,
+ QVBoxLayout,
+ QWidget,
+)
from aare.gui.panels.file_path_panel import FilePathPanel
from aare.gui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel
@@ -11,7 +20,6 @@ from aare.gui.panels.raster_data_collection import RasterDataCollectionPanel
from aare.gui.panels.rotation_data_collection import RotationDataCollectionPanel
from aare.gui.panels.smart_rotation_panel import SimpleRotationSettingsPanel
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
-from aare.gui.styles import ABORT_TEXT
from aare.gui.widgets.title_label import TitleLabel, tighten_column
@@ -42,39 +50,72 @@ class DataCollectionSettings(QFrame):
self.manual_sample_panel = ManualSamplePanel(self)
v_layout.addWidget(self.manual_sample_panel)
- self._tab_widget = QTabWidget()
+ # QTabBar + QStackedWidget instead of QTabWidget: the loop-centering
+ # button row must sit BETWEEN the tab bar and the pages, which a
+ # QTabWidget cannot host.
+ self._tab_bar = QTabBar(self)
+ self._stack = QStackedWidget(self)
self.raster = RasterDataCollectionPanel(
parent=self, raster_mgr=raster_mgr, diffraction=diffraction
)
- self._tab_widget.addTab(self.raster, "Raster scan")
-
self.screening = RotationDataCollectionPanel(parent=self, diffraction=diffraction)
- self._tab_widget.addTab(self.screening, "Rotation")
-
self.simple = SimpleRotationSettingsPanel(parent=self)
- self._tab_widget.addTab(self.simple, "Simple")
-
self.fluo = FluorescenceDataCollectionPanel(parent=self)
- self._tab_widget.addTab(self.fluo, "XRF")
+ for panel, label in (
+ (self.raster, "Raster scan"),
+ (self.screening, "Rotation"),
+ (self.simple, "Simple"),
+ (self.fluo, "XRF"),
+ ):
+ self._stack.addWidget(panel)
+ self._tab_bar.addTab(label)
+
+ # Ex-"Loop centering" panel buttons; always visible, whatever the tab.
+ self.find_tip = QPushButton("ML Loop Centring", parent=self)
+ self.bounding_box = QPushButton("Make Raster 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)
+
+ # Pane frame carries the border QTabWidget::pane used to draw
+ # (#expConfigPane rule in styles.py).
+ pane = QFrame(self)
+ pane.setObjectName("expConfigPane")
+ pane_layout = QVBoxLayout(pane)
+ # 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._stack)
# Own container: TitleLabel collapse hides its siblings, so without it
# "Exp. Config." would also swallow the dataset path and abort button.
exp_config = QWidget(self)
exp_config_layout = QVBoxLayout(exp_config)
exp_config_layout.setContentsMargins(0, 0, 0, 0)
- exp_config_layout.addWidget(TitleLabel("Exp. Config.", exp_config, collapsible=True))
- exp_config_layout.addWidget(self._tab_widget)
+ exp_config_layout.setSpacing(0) # tab bar flush on the pane, like QTabWidget
+ exp_config_layout.addWidget(
+ TitleLabel(
+ "Experiment configuration", exp_config, collapsible=True, default_collapsed=False
+ )
+ )
+ exp_config_layout.addWidget(self._tab_bar)
+ exp_config_layout.addWidget(pane)
v_layout.addWidget(exp_config)
- abort_button = QPushButton("Abort measurement", parent=self)
- abort_button.setStyleSheet(f"color: {ABORT_TEXT};")
- abort_button.clicked.connect(self.cancel_button_clicked)
- v_layout.addWidget(abort_button)
- # Stretch after the button: abort sits snug under the tabs instead of
- # being pinned to the bottom of the fixed-height column.
+ # Abort lives inside each tab now, under that tab's action buttons;
+ # all four feed the same cancel signal.
+ for panel in (self.raster, self.screening, self.simple, self.fluo):
+ panel.abort_button.clicked.connect(self.cancel_button_clicked)
v_layout.addStretch()
tighten_column(v_layout)
+ # Abort hugs the pane: undo the uniform bottom margin tighten_column
+ # just gave the Exp. Config. group.
+ m = exp_config_layout.contentsMargins()
+ exp_config_layout.setContentsMargins(m.left(), m.top(), m.right(), 0)
raster_mgr.update_filename(self.file_path_panel.filename)
self.screening.update_filename(self.file_path_panel.filename)
@@ -86,11 +127,27 @@ class DataCollectionSettings(QFrame):
self.file_path_panel.path_updated.connect(self.simple.update_filename)
self._sample_id = None
- self._tab_widget.currentChanged.connect(self._on_tab_changed)
+ self._tab_bar.currentChanged.connect(self._stack.setCurrentIndex)
+ self._tab_bar.currentChanged.connect(self._on_tab_changed)
+ self._tab_bar.currentChanged.connect(self._sync_stack_height)
+ self._sync_stack_height(self._tab_bar.currentIndex())
+
+ @Slot(int)
+ def _sync_stack_height(self, idx: int):
+ # QStackedWidget's sizeHint is its TALLEST page, which left a dead gap
+ # above the Abort button on shorter tabs. Ignored vertical policy on
+ # hidden pages makes the stack track only the current page's height.
+ for i in range(self._stack.count()):
+ page = self._stack.widget(i)
+ vertical = (
+ QSizePolicy.Policy.Preferred if i == idx else QSizePolicy.Policy.Ignored
+ )
+ page.setSizePolicy(QSizePolicy.Policy.Preferred, vertical)
+ self._stack.adjustSize()
@Slot()
def switch_to_raster(self):
- self._tab_widget.setCurrentIndex(0)
+ self._tab_bar.setCurrentIndex(0)
@Slot()
def cancel_button_clicked(self):
@@ -103,7 +160,7 @@ class DataCollectionSettings(QFrame):
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._tab_widget.setCurrentIndex(0)
+ self._tab_bar.setCurrentIndex(0)
@Slot(int)
def _on_tab_changed(self, idx: int):
diff --git a/src/aare/gui/panels/fluorescence_data_collection.py b/src/aare/gui/panels/fluorescence_data_collection.py
index ce3980e6..f0aa1df4 100644
--- a/src/aare/gui/panels/fluorescence_data_collection.py
+++ b/src/aare/gui/panels/fluorescence_data_collection.py
@@ -2,7 +2,7 @@ from aarecommon.models.models import FluorescenceSpectrumParameterModel
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import QCheckBox, QGridLayout, QLabel, QPushButton, QWidget
-from aare.gui.styles import GO_TEXT
+from aare.gui.styles import ABORT_TEXT, GO_TEXT
from aare.gui.widgets.number_line_edit import NumberLineEdit
@@ -38,6 +38,11 @@ class FluorescenceDataCollectionPanel(QWidget):
self.run_btn.setStyleSheet(f"color: {GO_TEXT};")
lay.addWidget(self.run_btn, 4, 0, 1, 3)
+ # Per-tab Abort (DataCollectionSettings wires it to the DAQ cancel).
+ self.abort_button = QPushButton("Abort measurement", self)
+ self.abort_button.setStyleSheet(f"color: {ABORT_TEXT};")
+ lay.addWidget(self.abort_button, 5, 0, 1, 3)
+
self.run_btn.clicked.connect(self._emit_params)
@Slot()
diff --git a/src/aare/gui/panels/monochromator_panel.py b/src/aare/gui/panels/monochromator_panel.py
index 2dc815a9..a5c76cc7 100644
--- a/src/aare/gui/panels/monochromator_panel.py
+++ b/src/aare/gui/panels/monochromator_panel.py
@@ -13,29 +13,31 @@ class MonochromatorPanel(QWidget):
super().__init__(parent)
grid_layout = QGridLayout(self)
- grid_layout.addWidget(TitleLabel("Monochromator", self, collapsible=True), 0, 0, 1, 2)
+ grid_layout.addWidget(TitleLabel("Monochromator", self, collapsible=True, default_collapsed=False), 0, 0, 1, 3)
self.mono_pitch_scan_button = QPushButton("Mono Pitch Scan", parent=self)
self.mono_pitch_scan_button.clicked.connect(self.mono_pitch_scan.emit)
- grid_layout.addWidget(self.mono_pitch_scan_button, 1, 0, 1, 2)
+ grid_layout.addWidget(self.mono_pitch_scan_button, 1, 0, 1, 3)
+ # One row (label | value | button) instead of three — vertical space.
+ # Display in keV; the DAQ API stays in eV (converted on emit).
grid_layout.addWidget(QLabel("Energy", parent=self), 2, 0)
self.energy_spin = QDoubleSpinBox(parent=self)
self.energy_spin.setDecimals(3)
- self.energy_spin.setRange(1000.0, 30000.0)
- self.energy_spin.setSingleStep(100.0)
- self.energy_spin.setSuffix(" eV")
- self.energy_spin.setValue(12000.0)
- grid_layout.addWidget(self.energy_spin, 3, 0)
+ self.energy_spin.setRange(1.0, 30.0)
+ self.energy_spin.setSingleStep(0.1)
+ self.energy_spin.setSuffix(" keV")
+ self.energy_spin.setValue(12.0)
+ grid_layout.addWidget(self.energy_spin, 2, 1)
self.change_energy_button = QPushButton("Change Energy", parent=self)
self.change_energy_button.clicked.connect(self._emit_change_energy)
- grid_layout.addWidget(self.change_energy_button, 3, 1)
+ grid_layout.addWidget(self.change_energy_button, 2, 2)
@Slot()
def _emit_change_energy(self):
- self.change_energy.emit(float(self.energy_spin.value()))
+ self.change_energy.emit(float(self.energy_spin.value()) * 1000.0)
@Slot(DAQStatusModel)
def update_daq_status(self, _status: DAQStatusModel):
diff --git a/src/aare/gui/panels/raster_data_collection.py b/src/aare/gui/panels/raster_data_collection.py
index 8bd6ff74..d272dbfd 100644
--- a/src/aare/gui/panels/raster_data_collection.py
+++ b/src/aare/gui/panels/raster_data_collection.py
@@ -7,15 +7,13 @@ from PySide6.QtWidgets import (
QLabel,
QMessageBox,
QPushButton,
- QSizePolicy,
QSlider,
- QSpacerItem,
)
from aare.gui.constants import LOGGER_NAME
from aare.gui.panels.scan_settings_panel import ScanSettingsPanel
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager, RasterGridMetric
-from aare.gui.styles import GO_TEXT
+from aare.gui.styles import ABORT_TEXT, GO_TEXT
from aare.gui.widgets.number_line_edit import DbOverrideLineEdit
from aare.gui.widgets.raster_grid_table import RasterGridTable
@@ -131,11 +129,6 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self._table = RasterGridTable(raster_mgr)
self._layout.addWidget(self._table, 9, 0, 1, 5)
- horizontal_spacer = QSpacerItem(
- 40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding
- )
- self._layout.addItem(horizontal_spacer, 10, 0, 1, 5)
-
self._layout.addWidget(QLabel("Measurement time", parent=self), 11, 0)
self.total_time = QLabel(f"{self._total_time} min 0 s")
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
@@ -152,6 +145,11 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self.auto_button.setStyleSheet(f"color: {GO_TEXT};")
self.auto_button.clicked.connect(self._on_evaluate_auto_clicked)
self._layout.addWidget(self.auto_button, 13, 0, 1, 5)
+
+ # Per-tab Abort (DataCollectionSettings wires it to the DAQ cancel).
+ self.abort_button = QPushButton("Abort measurement")
+ self.abort_button.setStyleSheet(f"color: {ABORT_TEXT};")
+ self._layout.addWidget(self.abort_button, 14, 0, 1, 5)
self._reset_to_defaults()
self.update_grid_scan_size()
diff --git a/src/aare/gui/panels/rotation_data_collection.py b/src/aare/gui/panels/rotation_data_collection.py
index 774fe9f5..c55dc304 100644
--- a/src/aare/gui/panels/rotation_data_collection.py
+++ b/src/aare/gui/panels/rotation_data_collection.py
@@ -10,7 +10,7 @@ from PySide6.QtWidgets import QComboBox, QLabel, QMessageBox, QPushButton
from aare.gui.constants import LOGGER_NAME
from aare.gui.panels.scan_settings_panel import ScanSettingsPanel
-from aare.gui.styles import GO_TEXT
+from aare.gui.styles import ABORT_TEXT, GO_TEXT
from aare.gui.widgets.number_line_edit import DbOverrideLineEdit, NumberLineEdit
logger = setup_logger(LOGGER_NAME)
@@ -164,6 +164,11 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
self.measurement_button.setStyleSheet(f"color: {GO_TEXT};")
self.measurement_button.clicked.connect(self.run_measurement)
self._layout.addWidget(self.measurement_button, 16, 0, 1, 6)
+
+ # Per-tab Abort (DataCollectionSettings wires it to the DAQ cancel).
+ self.abort_button = QPushButton("Abort measurement")
+ self.abort_button.setStyleSheet(f"color: {ABORT_TEXT};")
+ self._layout.addWidget(self.abort_button, 17, 0, 1, 6)
self._reset_to_defaults()
@Slot()
diff --git a/src/aare/gui/panels/samcam_panel.py b/src/aare/gui/panels/samcam_panel.py
index 77dfd9e7..0e91593f 100644
--- a/src/aare/gui/panels/samcam_panel.py
+++ b/src/aare/gui/panels/samcam_panel.py
@@ -4,6 +4,7 @@ from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDoubleSpinBox,
+ QGridLayout,
QHBoxLayout,
QLabel,
QLineEdit,
@@ -36,32 +37,28 @@ class SamcamPanel(QWidget):
# Create layout
layout = QVBoxLayout()
- layout.addWidget(TitleLabel("Sample camera", self, collapsible=True))
+ layout.addWidget(TitleLabel("Sample camera", self, collapsible=True, default_collapsed=False))
- # Exposure control
- exposure_layout = QHBoxLayout()
- exposure_label = QLabel("Exposure (s):")
+ # Exposure + gain share one row to save vertical space.
+ exposure_gain_layout = QHBoxLayout()
self.exposure_spinbox = QDoubleSpinBox()
self.exposure_spinbox.setRange(0, 1.0) # Adjust range as needed
self.exposure_spinbox.setSingleStep(0.001)
self.exposure_spinbox.setStyleSheet(f"QDoubleSpinBox {{ background-color: {INPUT_BG}; }}")
self.exposure_spinbox.setDecimals(3)
-
self.exposure_spinbox.valueChanged.connect(self._changed)
- exposure_layout.addWidget(exposure_label)
- exposure_layout.addWidget(self.exposure_spinbox)
- # Gain control
- gain_layout = QHBoxLayout()
- gain_label = QLabel("Gain:")
self.gain_spinbox = QDoubleSpinBox()
self.gain_spinbox.setRange(0, 1000) # Adjust range as needed
self.gain_spinbox.setSingleStep(1)
self.gain_spinbox.setDecimals(1)
self.gain_spinbox.setStyleSheet(f"QDoubleSpinBox {{ background-color: {INPUT_BG}; }}")
self.gain_spinbox.valueChanged.connect(self._changed)
- gain_layout.addWidget(gain_label)
- gain_layout.addWidget(self.gain_spinbox)
+
+ exposure_gain_layout.addWidget(QLabel("Exposure (s):"))
+ exposure_gain_layout.addWidget(self.exposure_spinbox)
+ exposure_gain_layout.addWidget(QLabel("Gain:"))
+ exposure_gain_layout.addWidget(self.gain_spinbox)
# Persist the current gain/exposure as the beam-location preset for the
# current zoom (only meaningful in beam-location mode).
@@ -86,56 +83,53 @@ class SamcamPanel(QWidget):
screenshot_message_layout.addWidget(screenshot_message_label)
screenshot_message_layout.addWidget(self.screenshot_message_edit)
- self.screenshot_button = QPushButton("Take screenshot")
+ self.screenshot_button = QPushButton("Save samcam image")
self.screenshot_button.clicked.connect(self._request_screenshot)
- # Show detections checkbox
- detections_layout = QHBoxLayout()
+ # Overlay checkboxes, two columns to save vertical space; related
+ # toggles share a row.
self.show_detections_checkbox = QCheckBox("Show ML detections")
self.show_detections_checkbox.setChecked(True) # Default to checked
self.show_detections_checkbox.toggled.connect(self.show_detections_changed.emit)
- detections_layout.addWidget(self.show_detections_checkbox)
- # Show detection polygons checkbox
- detection_polygons_layout = QHBoxLayout()
self.show_detection_polygons_checkbox = QCheckBox("Show ML polygons")
self.show_detection_polygons_checkbox.setChecked(True)
self.show_detection_polygons_checkbox.toggled.connect(
self.show_detection_polygons_changed.emit
)
- detection_polygons_layout.addWidget(self.show_detection_polygons_checkbox)
- # Show target point checkbox
- target_point_layout = QHBoxLayout()
self.show_target_point_checkbox = QCheckBox("Show target point")
self.show_target_point_checkbox.setChecked(True)
self.show_target_point_checkbox.toggled.connect(self.show_target_point_changed.emit)
- target_point_layout.addWidget(self.show_target_point_checkbox)
- # Show target coordinates checkbox
- target_coords_layout = QHBoxLayout()
self.show_target_coordinates_checkbox = QCheckBox("Show target coordinates")
self.show_target_coordinates_checkbox.setChecked(True)
self.show_target_coordinates_checkbox.toggled.connect(
self.show_target_coordinates_changed.emit
)
- target_coords_layout.addWidget(self.show_target_coordinates_checkbox)
- # Show legend checkbox
- legend_layout = QHBoxLayout()
self.show_overlay_legend_checkbox = QCheckBox("Show overlay legend")
self.show_overlay_legend_checkbox.setChecked(True)
self.show_overlay_legend_checkbox.toggled.connect(self.show_overlay_legend_changed.emit)
- legend_layout.addWidget(self.show_overlay_legend_checkbox)
- # Compact legend checkbox
- compact_legend_layout = QHBoxLayout()
self.compact_overlay_legend_checkbox = QCheckBox("Compact legend")
self.compact_overlay_legend_checkbox.setChecked(False)
self.compact_overlay_legend_checkbox.toggled.connect(
self.compact_overlay_legend_changed.emit
)
- compact_legend_layout.addWidget(self.compact_overlay_legend_checkbox)
+
+ checkbox_grid = QGridLayout()
+ for i, checkbox in enumerate(
+ (
+ self.show_detections_checkbox,
+ self.show_detection_polygons_checkbox,
+ self.show_target_point_checkbox,
+ self.show_target_coordinates_checkbox,
+ self.show_overlay_legend_checkbox,
+ self.compact_overlay_legend_checkbox,
+ )
+ ):
+ checkbox_grid.addWidget(checkbox, i // 2, i % 2)
# Target color
target_color_layout = QHBoxLayout()
@@ -148,18 +142,14 @@ class SamcamPanel(QWidget):
target_color_layout.addWidget(self.target_color_combo)
# Add controls to main layout
- layout.addLayout(exposure_layout)
- layout.addLayout(gain_layout)
- layout.addWidget(self.save_beam_location_button)
+ layout.addLayout(exposure_gain_layout)
+ samcam_buttons_layout = QHBoxLayout()
+ samcam_buttons_layout.addWidget(self.save_beam_location_button)
+ samcam_buttons_layout.addWidget(self.screenshot_button)
+ layout.addLayout(samcam_buttons_layout)
layout.addLayout(screenshot_filename_layout)
layout.addLayout(screenshot_message_layout)
- layout.addWidget(self.screenshot_button)
- layout.addLayout(detections_layout)
- layout.addLayout(detection_polygons_layout)
- layout.addLayout(target_point_layout)
- layout.addLayout(target_coords_layout)
- layout.addLayout(legend_layout)
- layout.addLayout(compact_legend_layout)
+ layout.addLayout(checkbox_grid)
layout.addLayout(target_color_layout)
self.setLayout(layout)
diff --git a/src/aare/gui/panels/scan_settings_panel.py b/src/aare/gui/panels/scan_settings_panel.py
index 39285963..3b570acd 100644
--- a/src/aare/gui/panels/scan_settings_panel.py
+++ b/src/aare/gui/panels/scan_settings_panel.py
@@ -63,10 +63,15 @@ class ScanSettingsPanel(QWidget):
# before, so they are unaffected by the wrapping.
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
+ outer.setSpacing(0)
outer.addWidget(self._build_source_toggle())
grid_host = QWidget(self)
self._layout = QGridLayout(grid_host)
+ # Toggle-to-grid gap = one grid row gap (top). Bottom 3 + the column's
+ # 3px spacing = one row gap between the last button and Abort too.
+ m = self._layout.contentsMargins()
+ self._layout.setContentsMargins(m.left(), 6, m.right(), 3)
outer.addWidget(grid_host)
self._layout.addWidget(QLabel("High resolution", parent=self), 0, 0)
diff --git a/src/aare/gui/panels/smart_rotation_panel.py b/src/aare/gui/panels/smart_rotation_panel.py
index 5b493d74..22855bef 100644
--- a/src/aare/gui/panels/smart_rotation_panel.py
+++ b/src/aare/gui/panels/smart_rotation_panel.py
@@ -8,7 +8,7 @@ from PySide6.QtWidgets import QGridLayout, QLabel, QPushButton, QSizePolicy, QSp
from aare.gui.constants import LOGGER_NAME
from aare.gui.panels.rotation_data_collection import add_data_to_path
-from aare.gui.styles import GO_TEXT, STATUS_ALERT
+from aare.gui.styles import ABORT_TEXT, GO_TEXT, STATUS_ALERT
from aare.gui.widgets.number_line_edit import NumberLineEdit
logger = setup_logger(LOGGER_NAME)
@@ -44,6 +44,10 @@ class SimpleRotationSettingsPanel(QWidget):
self._prev_params = SimpleScanParameters()
self._layout = QGridLayout(self)
+ # Top margin 0 like the Raster/Rotation pages (their toggle row sits
+ # at margin 0), so the gap under the ML-centring row matches.
+ m = self._layout.contentsMargins()
+ self._layout.setContentsMargins(m.left(), 0, m.right(), 3)
# Visible resolution (entry)
self._layout.addWidget(QLabel("Visible resolution", parent=self), 0, 0)
@@ -83,133 +87,138 @@ class SimpleRotationSettingsPanel(QWidget):
self._layout.addWidget(QLabel("K", parent=self), 4, 4)
self.temp_enter.newValue.connect(self.set_temperature)
+ # Run + Abort directly under the last configurable row; the read-only
+ # block below is reference info, not something to scroll past to act.
+ self.run_rotation_button = QPushButton("Run rotation", parent=self)
+ self.run_rotation_button.setStyleSheet(f"color: {GO_TEXT};")
+ self.run_rotation_button.clicked.connect(self.run_measurement)
+ self._layout.addWidget(self.run_rotation_button, 5, 0, 1, 6)
+
+ self.abort_button = QPushButton("Abort measurement", parent=self)
+ self.abort_button.setStyleSheet(f"color: {ABORT_TEXT};")
+ self._layout.addWidget(self.abort_button, 6, 0, 1, 6)
+
# Calculated labels
- self._layout.addWidget(QLabel("Target resolution", parent=self), 5, 0)
+ self._layout.addWidget(QLabel("Target resolution", parent=self), 7, 0)
self.target_res_label = QLabel("--", parent=self)
self.target_res_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.target_res_label, 5, 1, 1, 3)
- self._layout.addWidget(QLabel("Å", parent=self), 5, 4)
+ self._layout.addWidget(self.target_res_label, 7, 1, 1, 3)
+ self._layout.addWidget(QLabel("Å", parent=self), 7, 4)
- self._layout.addWidget(QLabel("Image time", parent=self), 6, 0)
+ self._layout.addWidget(QLabel("Image time", parent=self), 8, 0)
self.image_time_label = QLabel("--", parent=self)
self.image_time_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.image_time_label, 6, 1, 1, 3)
- self._layout.addWidget(QLabel("s", parent=self), 6, 4)
+ self._layout.addWidget(self.image_time_label, 8, 1, 1, 3)
+ self._layout.addWidget(QLabel("s", parent=self), 8, 4)
- self._layout.addWidget(QLabel("Transmission", parent=self), 7, 0)
+ self._layout.addWidget(QLabel("Transmission", parent=self), 9, 0)
self.transmission_label = QLabel("--", parent=self)
self.transmission_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.transmission_label, 7, 1, 1, 3)
- self._layout.addWidget(QLabel("%", parent=self), 7, 4)
+ self._layout.addWidget(self.transmission_label, 9, 1, 1, 3)
+ self._layout.addWidget(QLabel("%", parent=self), 9, 4)
- self._layout.addWidget(QLabel("Detector distance", parent=self), 8, 0)
+ self._layout.addWidget(QLabel("Detector distance", parent=self), 10, 0)
self.dtz_label = QLabel("--", parent=self)
self.dtz_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
- self._layout.addWidget(self.dtz_label, 8, 1, 1, 3)
- self._layout.addWidget(QLabel("mm", parent=self), 8, 4)
+ self._layout.addWidget(self.dtz_label, 10, 1, 1, 3)
+ self._layout.addWidget(QLabel("mm", parent=self), 10, 4)
- self._layout.addWidget(QLabel("Target Dose", parent=self), 9, 0)
+ self._layout.addWidget(QLabel("Target Dose", parent=self), 11, 0)
self.target_dose_label = QLabel("--", parent=self)
self.target_dose_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.target_dose_label, 9, 1, 1, 3)
- self._layout.addWidget(QLabel("MGy", parent=self), 9, 4)
+ self._layout.addWidget(self.target_dose_label, 11, 1, 1, 3)
+ self._layout.addWidget(QLabel("MGy", parent=self), 11, 4)
- self._layout.addWidget(QLabel("Calculated Dose Rate", parent=self), 10, 0)
+ self._layout.addWidget(QLabel("Calculated Dose Rate", parent=self), 12, 0)
self.calculated_dose_rate_label = QLabel("--", parent=self)
self.calculated_dose_rate_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.calculated_dose_rate_label, 10, 1, 1, 3)
- self._layout.addWidget(QLabel("MGy s-1", parent=self), 10, 4)
+ self._layout.addWidget(self.calculated_dose_rate_label, 12, 1, 1, 3)
+ self._layout.addWidget(QLabel("MGy s-1", parent=self), 12, 4)
- self._layout.addWidget(QLabel("Wilson B Factor", parent=self), 11, 0)
+ self._layout.addWidget(QLabel("Wilson B Factor", parent=self), 13, 0)
self.wilson_b_label = QLabel("--", parent=self)
self.wilson_b_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.wilson_b_label, 11, 1, 1, 3)
- self._layout.addWidget(QLabel("Å2", parent=self), 11, 4)
+ self._layout.addWidget(self.wilson_b_label, 13, 1, 1, 3)
+ self._layout.addWidget(QLabel("Å2", parent=self), 13, 4)
- self._layout.addWidget(QLabel("Crystal Size x", parent=self), 12, 0)
+ self._layout.addWidget(QLabel("Crystal Size x", parent=self), 14, 0)
self.xtal_x_label = QLabel("--", parent=self)
self.xtal_x_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
- self._layout.addWidget(self.xtal_x_label, 12, 1, 1, 3)
- self._layout.addWidget(QLabel("um", parent=self), 12, 4)
-
- self._layout.addWidget(QLabel("Crystal Size y", parent=self), 13, 0)
- self.xtal_y_label = QLabel("--", parent=self)
- self.xtal_y_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
- self._layout.addWidget(self.xtal_y_label, 13, 1, 1, 3)
- self._layout.addWidget(QLabel("um", parent=self), 13, 4)
-
- self._layout.addWidget(QLabel("Crystal Size z", parent=self), 14, 0)
- self.xtal_z_label = QLabel("--", parent=self)
- self.xtal_z_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
- self._layout.addWidget(self.xtal_z_label, 14, 1, 1, 3)
+ self._layout.addWidget(self.xtal_x_label, 14, 1, 1, 3)
self._layout.addWidget(QLabel("um", parent=self), 14, 4)
- self._layout.addWidget(QLabel("Calculated Dose (xtal size)", parent=self), 15, 0)
+ self._layout.addWidget(QLabel("Crystal Size y", parent=self), 15, 0)
+ self.xtal_y_label = QLabel("--", parent=self)
+ self.xtal_y_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+ self._layout.addWidget(self.xtal_y_label, 15, 1, 1, 3)
+ self._layout.addWidget(QLabel("um", parent=self), 15, 4)
+
+ self._layout.addWidget(QLabel("Crystal Size z", parent=self), 16, 0)
+ self.xtal_z_label = QLabel("--", parent=self)
+ self.xtal_z_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+ self._layout.addWidget(self.xtal_z_label, 16, 1, 1, 3)
+ self._layout.addWidget(QLabel("um", parent=self), 16, 4)
+
+ self._layout.addWidget(QLabel("Calculated Dose (xtal size)", parent=self), 17, 0)
self.xtal_size_dose_label = QLabel("--", parent=self)
self.xtal_size_dose_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.xtal_size_dose_label, 15, 1, 1, 3)
- self._layout.addWidget(QLabel("MGy", parent=self), 15, 4)
+ self._layout.addWidget(self.xtal_size_dose_label, 17, 1, 1, 3)
+ self._layout.addWidget(QLabel("MGy", parent=self), 17, 4)
- self._layout.addWidget(QLabel("X-ray Wavelength", parent=self), 16, 0)
+ self._layout.addWidget(QLabel("X-ray Wavelength", parent=self), 18, 0)
self.wavelength_label = QLabel("--", parent=self)
self.wavelength_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.wavelength_label, 16, 1, 1, 3)
- self._layout.addWidget(QLabel("Å", parent=self), 16, 4)
+ self._layout.addWidget(self.wavelength_label, 18, 1, 1, 3)
+ self._layout.addWidget(QLabel("Å", parent=self), 18, 4)
- self._layout.addWidget(QLabel("Flux", parent=self), 17, 0)
+ self._layout.addWidget(QLabel("Flux", parent=self), 19, 0)
self.flux_label = QLabel("--", parent=self)
self.flux_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
- self._layout.addWidget(self.flux_label, 17, 1, 1, 3)
- self._layout.addWidget(QLabel("x 109 ph s-1", parent=self), 17, 4)
+ self._layout.addWidget(self.flux_label, 19, 1, 1, 3)
+ self._layout.addWidget(QLabel("x 109 ph s-1", parent=self), 19, 4)
- self._layout.addWidget(QLabel("Beam Size", parent=self), 18, 0)
+ self._layout.addWidget(QLabel("Beam Size", parent=self), 20, 0)
self.beam_size_label = QLabel("--", parent=self)
self.beam_size_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.beam_size_label, 18, 1, 1, 3)
- self._layout.addWidget(QLabel("um2", parent=self), 18, 4)
+ self._layout.addWidget(self.beam_size_label, 20, 1, 1, 3)
+ self._layout.addWidget(QLabel("um2", parent=self), 20, 4)
- self._layout.addWidget(QLabel("Calculated Dose", parent=self), 19, 0)
+ self._layout.addWidget(QLabel("Calculated Dose", parent=self), 21, 0)
self.calculated_dose_label = QLabel("--", parent=self)
self.calculated_dose_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
- self._layout.addWidget(self.calculated_dose_label, 19, 1, 1, 3)
- self._layout.addWidget(QLabel("MGy", parent=self), 19, 4)
+ self._layout.addWidget(self.calculated_dose_label, 21, 1, 1, 3)
+ self._layout.addWidget(QLabel("MGy", parent=self), 21, 4)
- self._layout.addWidget(QLabel("Total measurement time", parent=self), 20, 0)
+ self._layout.addWidget(QLabel("Total measurement time", parent=self), 22, 0)
self.total_time = QLabel(f"{self.total_time_s} min 0 s")
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
- self._layout.addWidget(self.total_time, 20, 1, 1, 3)
+ self._layout.addWidget(self.total_time, 22, 1, 1, 3)
- # add vertical stretch between detector distance and the run button
+ # Vertical stretch below everything (surplus space sink).
self._layout.addItem(
- QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding), 21, 0, 1, 6
+ QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding), 23, 0, 1, 6
)
- # Run rotation button
- self.run_rotation_button = QPushButton("Run rotation", parent=self)
- self.run_rotation_button.setStyleSheet(f"color: {GO_TEXT};")
- self.run_rotation_button.clicked.connect(self.run_measurement)
- self._layout.addWidget(self.run_rotation_button, 22, 0, 1, 6)
-
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self._d = s
diff --git a/src/aare/gui/widgets/raster_grid_table.py b/src/aare/gui/widgets/raster_grid_table.py
index ee497610..7f76791d 100644
--- a/src/aare/gui/widgets/raster_grid_table.py
+++ b/src/aare/gui/widgets/raster_grid_table.py
@@ -33,8 +33,10 @@ class RasterGridTable(QTableWidget):
header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
- # Set minimum height
- self.setMinimumHeight(100)
+ # Exactly 5 rows of space; more grids scroll inside the table.
+ header_height = self.horizontalHeader().sizeHint().height()
+ row_height = self.verticalHeader().defaultSectionSize()
+ self.setFixedHeight(header_height + 3 * row_height + 1 * self.frameWidth())
# Connect to raster manager signals
self._raster_mgr.completed_grid_updated.connect(self.refresh_table)