feat: pending color while typing in Experiment configuration values
Values inside the Experiment configuration banner now show the same pending color as the motor move boxes while the typed text differs from the applied value; Enter (or focus-out) commits as before and clears it. Opt-in via NumberLineEdit(track_pending=True) because MotorMoveGroup owns the movestate property on its registered boxes; reuses the existing movestate QSS, so no new styling. The Set Energy spinbox marks pending until Change Energy is clicked, since its apply gate is the button. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,11 @@ class DataCollectionSettings(QFrame):
|
||||
self.energy_spin.setRange(1.0, 30.0)
|
||||
self.energy_spin.setSingleStep(0.1)
|
||||
self.energy_spin.setValue(12.0)
|
||||
# Pending color (same movestate QSS as the value fields) from edit
|
||||
# until Change Energy is clicked — this row's apply gate is the
|
||||
# button, not Enter, so the color marks "not sent yet".
|
||||
self._energy_sent = self.energy_spin.value()
|
||||
self.energy_spin.valueChanged.connect(self._on_energy_spin_changed)
|
||||
self.change_energy_button = QPushButton("Change Energy", parent=self)
|
||||
self.change_energy_button.clicked.connect(self._emit_change_energy)
|
||||
energy_row = QWidget(self)
|
||||
@@ -195,8 +200,24 @@ class DataCollectionSettings(QFrame):
|
||||
page.setSizePolicy(QSizePolicy.Policy.Preferred, vertical)
|
||||
self._stack.adjustSize()
|
||||
|
||||
def _set_energy_pending(self, pending: bool):
|
||||
# movestate lives on the spinbox's internal QLineEdit so the existing
|
||||
# QLineEdit[movestate="pending"] theme rules match without new QSS.
|
||||
box = self.energy_spin.lineEdit()
|
||||
state = "pending" if pending else ""
|
||||
if box.property("movestate") != state:
|
||||
box.setProperty("movestate", state)
|
||||
box.style().unpolish(box)
|
||||
box.style().polish(box)
|
||||
|
||||
@Slot(float)
|
||||
def _on_energy_spin_changed(self, value: float):
|
||||
self._set_energy_pending(value != self._energy_sent)
|
||||
|
||||
@Slot()
|
||||
def _emit_change_energy(self):
|
||||
self._energy_sent = self.energy_spin.value()
|
||||
self._set_energy_pending(False)
|
||||
self.change_energy.emit(float(self.energy_spin.value()) * 1000.0)
|
||||
|
||||
@Slot()
|
||||
|
||||
@@ -16,12 +16,16 @@ class FluorescenceDataCollectionPanel(QWidget):
|
||||
|
||||
# Beam transmission (0..1)
|
||||
lay.addWidget(QLabel("Beam transmission", self), 0, 0)
|
||||
self.transmission = NumberLineEdit(0.0, 1.0, decimals=4, default=0.1, parent=self)
|
||||
self.transmission = NumberLineEdit(
|
||||
0.0, 1.0, decimals=4, default=0.1, parent=self, track_pending=True
|
||||
)
|
||||
lay.addWidget(self.transmission, 0, 1)
|
||||
|
||||
# Exposure time (seconds)
|
||||
lay.addWidget(QLabel("Exposure time", self), 1, 0)
|
||||
self.exposure = NumberLineEdit(0.01, 60.0, decimals=3, default=1.0, parent=self)
|
||||
self.exposure = NumberLineEdit(
|
||||
0.01, 60.0, decimals=3, default=1.0, parent=self, track_pending=True
|
||||
)
|
||||
lay.addWidget(self.exposure, 1, 1)
|
||||
lay.addWidget(QLabel("s", self), 1, 2)
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
self._total_time = 0.0
|
||||
|
||||
self._layout.addWidget(QLabel("Start angle", parent=self), 3, 0)
|
||||
self.start_angle = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
|
||||
self.start_angle = NumberLineEdit(
|
||||
-720, 720.0, 0.0, decimals=3, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.start_angle, 3, 1, 1, 2)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 3, 3)
|
||||
|
||||
@@ -72,7 +74,9 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
)
|
||||
|
||||
self._layout.addWidget(QLabel("Image angle", parent=self), 5, 0)
|
||||
self.screening_image_angle = NumberLineEdit(0, 90.0, 0.5, decimals=3, parent=self)
|
||||
self.screening_image_angle = NumberLineEdit(
|
||||
0, 90.0, 0.5, decimals=3, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.screening_image_angle, 5, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 5, 4)
|
||||
|
||||
@@ -81,7 +85,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
"daq.data_collection_settings.default_screening_settings.exp_time_s", 0.1
|
||||
)
|
||||
self.screening_image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, default_screening_exp_time, decimals=4, parent=self
|
||||
0.0005, 10.0, default_screening_exp_time, decimals=4, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.screening_image_time_enter, 6, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("s", parent=self), 6, 4)
|
||||
|
||||
@@ -51,13 +51,17 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
|
||||
# Visible resolution (entry)
|
||||
self._layout.addWidget(QLabel("Visible resolution", parent=self), 0, 0)
|
||||
self.visible_res_enter = NumberLineEdit(0.8, 10.0, decimals=2, default=2.0, parent=self)
|
||||
self.visible_res_enter = NumberLineEdit(
|
||||
0.8, 10.0, decimals=2, default=2.0, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.visible_res_enter, 0, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("Å", parent=self), 0, 4)
|
||||
self.visible_res_enter.newValue.connect(self.set_visible_resolution)
|
||||
|
||||
self._layout.addWidget(QLabel("Start angle", parent=self), 1, 0)
|
||||
self.start_angle_enter = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
|
||||
self.start_angle_enter = NumberLineEdit(
|
||||
-720, 720.0, 0.0, decimals=3, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.start_angle_enter, 1, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 1, 4)
|
||||
|
||||
@@ -69,20 +73,24 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
# Angular range (entry)
|
||||
self._layout.addWidget(QLabel("Total angle", parent=self), 2, 0)
|
||||
self.angular_range_enter = NumberLineEdit(
|
||||
5.0, 1000.0, decimals=3, default=360.0, parent=self
|
||||
5.0, 1000.0, decimals=3, default=360.0, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.angular_range_enter, 2, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 2, 4)
|
||||
self.visible_res_enter.newValue.connect(self.set_total_angle)
|
||||
|
||||
self._layout.addWidget(QLabel("Image angle", parent=self), 3, 0)
|
||||
self.image_angle_enter = NumberLineEdit(0.001, 1.000, decimals=3, default=0.2, parent=self)
|
||||
self.image_angle_enter = NumberLineEdit(
|
||||
0.001, 1.000, decimals=3, default=0.2, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.image_angle_enter, 3, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 3, 4)
|
||||
self.image_angle_enter.newValue.connect(self.set_image_angle)
|
||||
|
||||
self._layout.addWidget(QLabel("Temperature", parent=self), 4, 0)
|
||||
self.temp_enter = NumberLineEdit(80, 330, decimals=2, default=100.0, parent=self)
|
||||
self.temp_enter = NumberLineEdit(
|
||||
80, 330, decimals=2, default=100.0, parent=self, track_pending=True
|
||||
)
|
||||
self._layout.addWidget(self.temp_enter, 4, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("K", parent=self), 4, 4)
|
||||
self.temp_enter.newValue.connect(self.set_temperature)
|
||||
|
||||
@@ -6,16 +6,32 @@ from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QLineEdit, QWidget
|
||||
class NumberLineEdit(QLineEdit):
|
||||
"""Colors are centralized: the per-theme INPUT rules in styles.py key on
|
||||
the :read-only pseudo-class and the "invalid" dynamic property set here —
|
||||
no inline stylesheets, so both themes restyle these fields."""
|
||||
no inline stylesheets, so both themes restyle these fields.
|
||||
|
||||
track_pending=True additionally shows the "movestate" pending color (same
|
||||
QSS as the motor move boxes) while the typed text differs from the last
|
||||
committed value; Enter/focus-out commits and clears it. Opt-in because
|
||||
MotorMoveGroup owns the movestate property on the boxes it registers."""
|
||||
|
||||
newValue = Signal(float)
|
||||
|
||||
def __init__(
|
||||
self, min_val: float, max_val: float, default: float = 0.0, decimals: int = 2, parent=None
|
||||
self,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
default: float = 0.0,
|
||||
decimals: int = 2,
|
||||
parent=None,
|
||||
track_pending: bool = False,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._read_only: bool = False
|
||||
self._is_valid: bool = True
|
||||
self._track_pending: bool = track_pending
|
||||
self._applied_value: float = default
|
||||
if track_pending:
|
||||
# textEdited is user-only: programmatic setText never marks pending.
|
||||
self.textEdited.connect(self._on_pending_edit)
|
||||
|
||||
# Use a QDoubleValidator to only allow valid floating point numbers.
|
||||
# Named range_validator: plain "validator" would shadow
|
||||
@@ -49,6 +65,28 @@ class NumberLineEdit(QLineEdit):
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
def _set_pending(self, pending: bool) -> None:
|
||||
if not self._track_pending:
|
||||
return
|
||||
state = "pending" if pending else ""
|
||||
if self.property("movestate") == state:
|
||||
return
|
||||
self.setProperty("movestate", state)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
@Slot(str)
|
||||
def _on_pending_edit(self, text: str):
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError:
|
||||
# incomplete entry ("", "-", "1e"): pending until it parses
|
||||
self._set_pending(True)
|
||||
return
|
||||
# string compare so "200" matches an applied "200.00"; typing back the
|
||||
# committed value cancels the pending state (same rule as MotorMoveGroup)
|
||||
self._set_pending(self.to_string(value) != self.to_string(self._applied_value))
|
||||
|
||||
@Slot(str)
|
||||
def on_text_changed(self, text: str):
|
||||
# when text changes check validation and change the colour of the line edit
|
||||
@@ -58,6 +96,8 @@ class NumberLineEdit(QLineEdit):
|
||||
@Slot()
|
||||
def on_editing_finished(self):
|
||||
val = float(self.text())
|
||||
self._applied_value = val
|
||||
self._set_pending(False)
|
||||
self.newValue.emit(val)
|
||||
self.saved_value = self.saved_value
|
||||
|
||||
@@ -72,6 +112,9 @@ class NumberLineEdit(QLineEdit):
|
||||
self.saved_value = val
|
||||
self.setText(self.to_string(val))
|
||||
self.blockSignals(False)
|
||||
# programmatic rewrite is a commit: the shown value IS the applied one
|
||||
self._applied_value = val
|
||||
self._set_pending(False)
|
||||
|
||||
def force_update_value(self, val: float):
|
||||
# Always update text and saved_value
|
||||
@@ -79,6 +122,9 @@ class NumberLineEdit(QLineEdit):
|
||||
self.saved_value = val
|
||||
self.setText(self.to_string(val))
|
||||
self.blockSignals(False)
|
||||
# programmatic rewrite is a commit: the shown value IS the applied one
|
||||
self._applied_value = val
|
||||
self._set_pending(False)
|
||||
|
||||
@Slot(float, float)
|
||||
def update_limits(self, min_val: float, max_val: float):
|
||||
@@ -272,7 +318,9 @@ class DbOverrideLineEdit(QWidget):
|
||||
self._source = self.SOURCE_DB
|
||||
self._busy = False
|
||||
|
||||
self.editor = NumberLineEdit(min_val, max_val, default, decimals, self)
|
||||
# pending color while typing: every DbOverride field lives in the
|
||||
# Experiment configuration group, which opted into the feedback
|
||||
self.editor = NumberLineEdit(min_val, max_val, default, decimals, self, track_pending=True)
|
||||
self.editor.newValue.connect(self._on_editor_value)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
|
||||
Reference in New Issue
Block a user