fix: energy spin movestate colors never rendered

The movestate property sat on the spinbox's inner QLineEdit, which both
themes deliberately paint transparent (QAbstractSpinBox QLineEdit rule)
- the state machine worked but no color ever showed, and the tests only
asserted the property, not the paint. The property now lives on the
spinbox itself with QAbstractSpinBox[movestate=...] added to both
themes' rules, verified by a pixel-sampling regression test that grabs
the rendered widget through the real stylesheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 14:03:06 +02:00
co-authored by Claude Fable 5
parent b8a3d3b831
commit 9db0f0b898
5 changed files with 60 additions and 16 deletions
+8 -5
View File
@@ -744,12 +744,15 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
}
/* Motor batch-move states (before the invalid rules on purpose: equal
specificity, so out-of-range red must come last to win). */
QLineEdit[movestate="pending"] {
specificity, so out-of-range red must come last to win). Spinboxes
(Set Energy) carry the property on the box itself — their inner
QLineEdit is transparent by design (rule below), so it can't show
the state. */
QLineEdit[movestate="pending"], QAbstractSpinBox[movestate="pending"] {
background-color: $input_pending_bg;
}
QLineEdit[movestate="moving"] {
QLineEdit[movestate="moving"], QAbstractSpinBox[movestate="moving"] {
background-color: $input_moving_bg;
}
@@ -1429,11 +1432,11 @@ def _sunset_stylesheet() -> str:
background-color: $dark_disabled;
}
QLineEdit[movestate="pending"] {
QLineEdit[movestate="pending"], QAbstractSpinBox[movestate="pending"] {
background-color: $dark_input_pending_bg;
}
QLineEdit[movestate="moving"] {
QLineEdit[movestate="moving"], QAbstractSpinBox[movestate="moving"] {
background-color: $dark_input_moving_bg;
}
+8 -9
View File
@@ -26,10 +26,10 @@ class SpinMoveState(QObject):
"""MotorMoveGroup's state machine for a single QDoubleSpinBox whose apply
gate is a button (the Set Energy rows): neutral tracks the readback, a
user edit stages pending (orange) and enables the button, the click turns
moving (green), and arrival within tol returns to neutral. Colors ride the
same movestate QSS via the spinbox's internal QLineEdit, so no new
styling. The caller keeps its own clicked connection for the actual send;
this object only tracks state."""
moving (green), and arrival within tol returns to neutral. The movestate
property sits on the spinbox itself — its inner QLineEdit is transparent
by theme design, so coloring it shows nothing. The caller keeps its own
clicked connection for the actual send; this object only tracks state."""
def __init__(self, spin: QDoubleSpinBox, button: QPushButton, tol: float, parent=None):
super().__init__(parent)
@@ -76,11 +76,10 @@ class SpinMoveState(QObject):
self._syncing = False
def _set_state(self, state: str):
box = self._spin.lineEdit()
if box.property("movestate") != state:
box.setProperty("movestate", state)
box.style().unpolish(box)
box.style().polish(box)
if self._spin.property("movestate") != state:
self._spin.setProperty("movestate", state)
self._spin.style().unpolish(self._spin)
self._spin.style().polish(self._spin)
self._state = state
self._button.setEnabled(state == "pending")
@@ -359,7 +359,7 @@ def test_energy_spin_motor_move_semantics(settings_panel, daq_status_factory):
# Same MotorMoveGroup wiring as the Beamline setup row (full state walk
# tested there); here: stage -> pending, button sends -> moving, readback
# arrival -> neutral.
box = settings_panel.energy_spin.lineEdit()
box = settings_panel.energy_spin # movestate sits on the spin itself
assert not settings_panel.change_energy_button.isEnabled()
settings_panel.update_daq_status(daq_status_factory()) # readback 12.0 keV
+1 -1
View File
@@ -55,7 +55,7 @@ def test_energy_spin_motor_move_semantics(qtbot, daq_status_factory):
# sends, and the color walks neutral -> pending -> moving -> neutral.
panel = MonochromatorPanel()
qtbot.addWidget(panel)
box = panel.energy_spin.lineEdit()
box = panel.energy_spin # movestate sits on the spin, not its inner edit
sent = []
panel.change_energy.connect(sent.append)
assert not panel.change_energy_button.isEnabled() # nothing staged yet
+42
View File
@@ -92,3 +92,45 @@ def test_update_limits_reranges_box(chi, qtbot):
assert box.range_validator.bottom() == -1.0
assert box.range_validator.top() == 50.0
assert "50" in box.toolTip()
def test_spin_move_state_colors_actually_render(qtbot):
# Regression: movestate used to sit on the spin's inner QLineEdit, which
# the theme paints transparent — the property asserts passed while no
# color ever showed. Sample real pixels through the theme stylesheet.
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QDoubleSpinBox, QPushButton
from aare.gui.styles import (
INPUT_MOVING_BG,
INPUT_PENDING_BG,
THEME_SUNRISE,
build_app_stylesheet,
)
from aare.gui.widgets.motor_move_group import SpinMoveState
spin = QDoubleSpinBox()
spin.setRange(4.0, 20.0)
spin.setStyleSheet(build_app_stylesheet(THEME_SUNRISE))
button = QPushButton()
qtbot.addWidget(spin)
qtbot.addWidget(button)
state = SpinMoveState(spin, button, tol=0.01)
spin.resize(140, 24)
spin.show()
def value_area_color():
img = spin.grab().toImage()
return img.pixelColor(20, img.height() // 2)
state.update_actual(12.0)
neutral = value_area_color()
spin.setValue(12.4) # user staging
assert value_area_color() == QColor(INPUT_PENDING_BG)
button.click()
assert value_area_color() == QColor(INPUT_MOVING_BG)
state.update_actual(12.398) # arrived within tol
assert value_area_color() == neutral