feat: breathe the pending transition target in the state strip

While Moving, the target state sat solid blue and read as already
reached. An opacity pulse (QGraphicsOpacityEffect + QPropertyAnimation)
marks it as in progress; a stylesheet animation would repolish the
button every frame, which the guarded updates in _apply_highlight
exist to avoid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
duan_jandClaude Fable 5 committed 2026-08-17 13:58:24 +02:00
1 parent 111c6885f9
commit 5637bfc075
2 files changed
+55 -2

No files matched your search

+39 -2
View File
@@ -1,9 +1,18 @@
from typing import ClassVar
from aarecommon.models.models import BeamlineStateEnum, DAQStatusModel
from PySide6.QtCore import Qt, QTimer, Signal, Slot
from PySide6.QtCore import QByteArray, QPropertyAnimation, Qt, QTimer, Signal, Slot
from PySide6.QtGui import QCursor, QFont, QFontMetrics
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QMenu, QPushButton, QSizePolicy, QToolTip
from PySide6.QtWidgets import (
QFrame,
QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QMenu,
QPushButton,
QSizePolicy,
QToolTip,
)
from aare.gui.styles import FONT_VALUE, THEME_SUNRISE, state_colors
@@ -133,6 +142,7 @@ class BeamlineStatePanel(QFrame):
self._hovered_state: BeamlineStateEnum | None = None
self._pending_target_state: BeamlineStateEnum | None = None
self._busy = False
self._breathing_button: HoverableButton | None = None
# After 3 s of hovering an unavailable state, explain which states
# it can be reached from.
@@ -322,8 +332,31 @@ class BeamlineStatePanel(QFrame):
self._style_separator(separator)
self._apply_highlight()
def _sync_breathing(self, button: HoverableButton | None) -> None:
# The transition target used to sit solid blue while Moving, reading
# as "already there". Breathe it via an opacity effect instead of a
# stylesheet animation: restyling would repolish the button per frame.
if button is self._breathing_button:
return
if self._breathing_button is not None:
# Qt deletes the old effect (and the animation parented to it).
self._breathing_button.setGraphicsEffect(None) # type: ignore[arg-type]
self._breathing_button = button
if button is None:
return
effect = QGraphicsOpacityEffect(button)
button.setGraphicsEffect(effect)
animation = QPropertyAnimation(effect, QByteArray(b"opacity"), effect)
animation.setDuration(1600)
animation.setStartValue(1.0)
animation.setKeyValueAt(0.5, 0.35)
animation.setEndValue(1.0)
animation.setLoopCount(-1)
animation.start()
def _apply_highlight(self) -> None:
available = self._available_targets()
pending_button: HoverableButton | None = None
for state, button in self._buttons.items():
is_current = state == self._current_state
is_pending = (
@@ -331,6 +364,8 @@ class BeamlineStatePanel(QFrame):
and self._current_state == BeamlineStateEnum.Moving
)
is_available = state in available
if is_pending:
pending_button = button
# Availability drives the look: active = bold (red for
# Maintenance, blue otherwise), reachable = orange, rest = grey.
@@ -391,6 +426,8 @@ class BeamlineStatePanel(QFrame):
if button.toolTip() != tooltip:
button.setToolTip(tooltip)
self._sync_breathing(pending_button)
def set_current_state(self, state: BeamlineStateEnum | None) -> None:
self._current_state = state
if (
@@ -121,3 +121,19 @@ def test_pending_target_cleared_on_arrival(qtbot):
assert panel._pending_target_state == BeamlineStateEnum.SampleExchange
panel.set_current_state(BeamlineStateEnum.SampleExchange)
assert panel._pending_target_state is None
def test_pending_target_breathes_only_while_moving(qtbot):
panel = _panel(qtbot)
panel.set_current_state(BeamlineStateEnum.Maintenance)
panel._emit_for_state(BeamlineStateEnum.SampleExchange)
target = panel._buttons[BeamlineStateEnum.SampleExchange]
assert target.graphicsEffect() is None # not Moving yet
panel.set_current_state(BeamlineStateEnum.Moving)
assert panel._breathing_button is target
assert target.graphicsEffect() is not None
panel.set_current_state(BeamlineStateEnum.SampleExchange)
assert panel._breathing_button is None
assert target.graphicsEffect() is None