From 1160fca1e86bbab1afa067314ea61918232e7b92 Mon Sep 17 00:00:00 2001 From: Dawn Date: Fri, 7 Aug 2026 17:08:40 +0200 Subject: [PATCH] style: compact the control panels Vertical-space pass: samcam merges exposure+gain onto one row, ABR and monochromator tighten their grids (energy now displayed in keV, DAQ API stays eV), beam-mark's clear button shares the readings row, the data collection pages move Run/Abort under the last configurable row and the tab area becomes QTabBar+QStackedWidget, the raster grid table gets a fixed height, and the automation panel drops its own title since the dock title bar already says it. Control panels start expanded via default_collapsed=False. Co-Authored-By: Claude Fable 5 --- src/aare/gui/panels/abr_tweak_panel.py | 32 +++-- src/aare/gui/panels/automation_panel.py | 78 ++++++----- src/aare/gui/panels/beam_mark_panel.py | 7 +- .../gui/panels/data_collection_settings.py | 99 ++++++++++--- .../panels/fluorescence_data_collection.py | 7 +- src/aare/gui/panels/monochromator_panel.py | 20 +-- src/aare/gui/panels/raster_data_collection.py | 14 +- .../gui/panels/rotation_data_collection.py | 7 +- src/aare/gui/panels/samcam_panel.py | 72 +++++----- src/aare/gui/panels/scan_settings_panel.py | 5 + src/aare/gui/panels/smart_rotation_panel.py | 131 ++++++++++-------- src/aare/gui/widgets/raster_grid_table.py | 6 +- 12 files changed, 285 insertions(+), 193 deletions(-) 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)