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:
1 parent
111c6885f9
commit
5637bfc075
2 files changed
+55
-2
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user