diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py index 7d94a09d..41254aeb 100644 --- a/src/aare/gui/styles.py +++ b/src/aare/gui/styles.py @@ -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; } diff --git a/src/aare/gui/widgets/motor_move_group.py b/src/aare/gui/widgets/motor_move_group.py index 05e3c5c7..3315d4b0 100644 --- a/src/aare/gui/widgets/motor_move_group.py +++ b/src/aare/gui/widgets/motor_move_group.py @@ -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") diff --git a/tests/unit/gui/test_data_collection_settings.py b/tests/unit/gui/test_data_collection_settings.py index 32ac0170..51cd0eaf 100644 --- a/tests/unit/gui/test_data_collection_settings.py +++ b/tests/unit/gui/test_data_collection_settings.py @@ -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 diff --git a/tests/unit/gui/test_monochromator_panel.py b/tests/unit/gui/test_monochromator_panel.py index 463756ab..46f2a814 100644 --- a/tests/unit/gui/test_monochromator_panel.py +++ b/tests/unit/gui/test_monochromator_panel.py @@ -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 diff --git a/tests/unit/gui/test_motor_move_group.py b/tests/unit/gui/test_motor_move_group.py index 0eee1b70..8f0653db 100644 --- a/tests/unit/gui/test_motor_move_group.py +++ b/tests/unit/gui/test_motor_move_group.py @@ -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