feat(xrayeye): reorganize control panel; exposure time as slider, not spinbox

Moves the auto-exposure/auto-gain toggles out of the shutter/camera
switch grid into their own section, bracketed by horizontal separators:
switches -> line -> exposure/gain section -> line -> alignment values
(2D positioner + zoom).

Also replaces the exposure-time QDoubleSpinBox with a QSlider bounded
to the device's real exposure_time_min/max (from IDSCamera, seeded
from hardware) instead of a hardcoded 0.01-1000ms placeholder --
QSlider is int-only, so the widget tracks tenths of a ms internally
and shows one decimal on a companion label. Submission still fires
once on release, not per tick.
This commit is contained in:
x01dc authored and x12sa committed 2026-09-15 08:31:49 +02:00
1 parent 8c1c322393
commit 557ca42a0c
1 file changed
+77 -27
@@ -13,7 +13,6 @@ from bec_widgets.widgets.plots.roi.image_roi import BaseROI, CircularROI, Rectan
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
from qtpy.QtCore import Qt, QTimer
from qtpy.QtWidgets import (
QDoubleSpinBox,
QFrame,
QGridLayout,
QHBoxLayout,
@@ -21,6 +20,7 @@ from qtpy.QtWidgets import (
QLineEdit,
QPushButton,
QSizePolicy,
QSlider,
QSpinBox,
QToolButton,
QVBoxLayout,
@@ -270,6 +270,10 @@ class OMNY_XRayEye(BECWidget, QWidget):
PIXEL_CALIBRATION_USER_PARAM = "pixel_calibration"
PIXEL_CALIBRATION_DEFAULT = 1.0
# exposure_time_slider works in tenths of a ms internally (QSlider is
# int-only); divide by this to get ms.
_EXPOSURE_SLIDER_SCALE = 10
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
self._live_view_signal = CAMERA[1]
@@ -403,8 +407,25 @@ class OMNY_XRayEye(BECWidget, QWidget):
switch_grid.addWidget(self.smear_preview_label, 1, 3, _right_vcenter)
switch_grid.addWidget(self.smear_preview_toggle, 1, 4, Qt.AlignmentFlag.AlignVCenter)
# Exposure/gain control knobs (row 2): auto-exposure and auto-gain
# enable toggles, mirroring the shutter/camera-running row above.
self.control_panel_layout.addWidget(self.switch_grid_widget)
# separator: below shutter/camera-running/smear switches, above the
# exposure/gain section
self.control_panel_layout.addWidget(self._create_separator())
# Exposure/gain section: auto-exposure and auto-gain enable toggles
# (row 0, same layout idiom as switch_grid above), plus a manual
# exposure-time slider (row 1) that's only enabled while
# auto-exposure is off. Its range is a placeholder until seeded from
# the hardware-reported (min, max) at connect time -- see
# getting_camera_status().
self.exposure_grid_widget = QWidget(parent=self)
exposure_grid = QGridLayout(self.exposure_grid_widget)
exposure_grid.setContentsMargins(0, 0, 0, 0)
exposure_grid.setHorizontalSpacing(8)
exposure_grid.setVerticalSpacing(4)
exposure_grid.setColumnStretch(0, 1)
self.auto_exposure_label = QLabel("Auto exposure", parent=self)
self.auto_exposure_toggle = ToggleSwitch(parent=self)
self.auto_exposure_toggle.checked = True
@@ -415,14 +436,35 @@ class OMNY_XRayEye(BECWidget, QWidget):
self.auto_gain_toggle.checked = True
self.auto_gain_toggle.enabled.connect(self.auto_gain_enabled_changed)
switch_grid.addWidget(self.auto_exposure_label, 2, 1, _right_vcenter)
switch_grid.addWidget(self.auto_exposure_toggle, 2, 2, Qt.AlignmentFlag.AlignVCenter)
switch_grid.addWidget(self.auto_gain_label, 2, 3, _right_vcenter)
switch_grid.addWidget(self.auto_gain_toggle, 2, 4, Qt.AlignmentFlag.AlignVCenter)
exposure_grid.addWidget(self.auto_exposure_label, 0, 1, _right_vcenter)
exposure_grid.addWidget(self.auto_exposure_toggle, 0, 2, Qt.AlignmentFlag.AlignVCenter)
exposure_grid.addWidget(self.auto_gain_label, 0, 3, _right_vcenter)
exposure_grid.addWidget(self.auto_gain_toggle, 0, 4, Qt.AlignmentFlag.AlignVCenter)
self.control_panel_layout.addWidget(self.switch_grid_widget)
# QSlider only takes ints, so exposure time is tracked in tenths of a
# ms internally (_EXPOSURE_SLIDER_SCALE) for one decimal of
# resolution on the display label.
self.exposure_time_label = QLabel("Exposure time", parent=self)
self.exposure_time_slider = QSlider(Qt.Orientation.Horizontal, parent=self)
self.exposure_time_slider.setRange(0, 10000) # placeholder; reseeded on connect
self.exposure_time_slider.setEnabled(False) # auto-exposure starts enabled
self.exposure_time_value_label = QLabel("-- ms", parent=self)
self.exposure_time_value_label.setMinimumWidth(60)
self.exposure_time_slider.valueChanged.connect(self._update_exposure_time_value_label)
# sliderReleased (fires once, on mouse-up), not valueChanged (fires on
# every tick while dragging) -- the throttling choice here, same
# "don't hammer the device" concern as editingFinished was for the
# spinbox this replaces.
self.exposure_time_slider.sliderReleased.connect(self.exposure_time_submitted)
# separator
exposure_grid.addWidget(self.exposure_time_label, 1, 1, _right_vcenter)
exposure_grid.addWidget(self.exposure_time_slider, 1, 2, 1, 2)
exposure_grid.addWidget(self.exposure_time_value_label, 1, 4, Qt.AlignmentFlag.AlignVCenter)
self.control_panel_layout.addWidget(self.exposure_grid_widget)
# separator: below the exposure/gain section, above the alignment
# values (2D positioner + zoom)
self.control_panel_layout.addWidget(self._create_separator())
# 2D Positioner (fixed size)
@@ -457,18 +499,6 @@ class OMNY_XRayEye(BECWidget, QWidget):
# Add form to control panel
self.control_panel_layout.addLayout(step_size_form)
# Exposure time (manual entry, only usable while auto-exposure is off)
exposure_form = QGridLayout()
self.exposure_time_spin = QDoubleSpinBox(parent=self)
self.exposure_time_spin.setRange(0.01, 1000.0) # ms; confirm real driver range
self.exposure_time_spin.setDecimals(2)
self.exposure_time_spin.setSuffix(" ms")
self.exposure_time_spin.setEnabled(False) # auto-exposure starts enabled
self.exposure_time_spin.editingFinished.connect(self.exposure_time_submitted)
exposure_form.addWidget(QLabel("Exposure time", parent=self), 0, 0)
exposure_form.addWidget(self.exposure_time_spin, 0, 1)
self.control_panel_layout.addLayout(exposure_form)
# Push form to bottom
self.control_panel_layout.addStretch()
@@ -934,7 +964,7 @@ class OMNY_XRayEye(BECWidget, QWidget):
enabled = bool(auto_exp.get("value"))
self.auto_exposure_toggle.blockSignals(True)
self.auto_exposure_toggle.checked = enabled
self.exposure_time_spin.setEnabled(not enabled)
self.exposure_time_slider.setEnabled(not enabled)
self.auto_exposure_toggle.blockSignals(False)
auto_gain = signals.get(f"{CAMERA[0]}_auto_gain_enabled")
@@ -943,11 +973,25 @@ class OMNY_XRayEye(BECWidget, QWidget):
self.auto_gain_toggle.checked = bool(auto_gain.get("value"))
self.auto_gain_toggle.blockSignals(False)
# Reseed the slider's bounds first (rare -- only changes if the pixel
# clock changed), so a value arriving in the same message is clamped
# against up-to-date min/max rather than a stale range.
exposure_min = signals.get(f"{CAMERA[0]}_exposure_time_min")
exposure_max = signals.get(f"{CAMERA[0]}_exposure_time_max")
if exposure_min is not None and exposure_max is not None:
lo = round(float(exposure_min.get("value")) * self._EXPOSURE_SLIDER_SCALE)
hi = round(float(exposure_max.get("value")) * self._EXPOSURE_SLIDER_SCALE)
if (lo, hi) != (self.exposure_time_slider.minimum(), self.exposure_time_slider.maximum()):
self.exposure_time_slider.setRange(lo, hi)
exposure_time = signals.get(f"{CAMERA[0]}_exposure_time")
if exposure_time is not None:
self.exposure_time_spin.blockSignals(True)
self.exposure_time_spin.setValue(float(exposure_time.get("value")))
self.exposure_time_spin.blockSignals(False)
self.exposure_time_slider.blockSignals(True)
self.exposure_time_slider.setValue(
round(float(exposure_time.get("value")) * self._EXPOSURE_SLIDER_SCALE)
)
self.exposure_time_slider.blockSignals(False)
self._update_exposure_time_value_label(self.exposure_time_slider.value())
@SafeSlot(bool)
def auto_exposure_enabled_changed(self, enabled: bool):
@@ -957,7 +1001,7 @@ class OMNY_XRayEye(BECWidget, QWidget):
self.auto_exposure_toggle.blockSignals(True)
self.dev.get(CAMERA[0]).auto_exposure_enabled.put(enabled)
self.auto_exposure_toggle.checked = enabled
self.exposure_time_spin.setEnabled(not enabled)
self.exposure_time_slider.setEnabled(not enabled)
self.auto_exposure_toggle.blockSignals(False)
@SafeSlot(bool)
@@ -970,8 +1014,14 @@ class OMNY_XRayEye(BECWidget, QWidget):
self.auto_gain_toggle.checked = enabled
self.auto_gain_toggle.blockSignals(False)
def _update_exposure_time_value_label(self, raw_value: int):
self.exposure_time_value_label.setText(
f"{raw_value / self._EXPOSURE_SLIDER_SCALE:.1f} ms"
)
def exposure_time_submitted(self):
self.dev.get(CAMERA[0]).exposure_time.put(self.exposure_time_spin.value())
value_ms = self.exposure_time_slider.value() / self._EXPOSURE_SLIDER_SCALE
self.dev.get(CAMERA[0]).exposure_time.put(value_ms)
@SafeSlot(bool)
def opening_shutter(self, enabled: bool):