Flash Animation System
Game engine-style O(1) per-window flash animations for UI feedback.
Module: pyqt_reactive.animation.flash_mixin
Overview
The flash animation system provides visual feedback when configuration values change. It uses a game engine architecture to achieve O(1) rendering per window regardless of how many elements are flashing.
Architecture
The system consists of three core components:
_GlobalFlashCoordinator (singleton): ONE 60fps timer for ALL windows
WindowFlashOverlay (per-window): Renders ALL flash rectangles in ONE paintEvent
FlashMixin (per-widget): API for registering elements and triggering flashes
┌─────────────────────────────────────────────────────────────┐
│ _GlobalFlashCoordinator │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ _flash_start_ │ │ _computed_colors: Dict[key, QColor] │
│ │ times: Dict │ │ (pre-computed each tick) │ │
│ └─────────────────┘ └──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ [60fps timer tick] │
│ │ │
└──────────────────────────────┼──────────────────────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│WindowFlashOverlay│ │WindowFlashOverlay│ │WindowFlashOverlay│
│ (Window A) │ │ (Window B) │ │ (Window C) │
│ │ │ │ │ │
│ ONE paintEvent│ │ ONE paintEvent│ │ ONE paintEvent│
│ renders ALL │ │ renders ALL │ │ renders ALL │
│ flash rects │ │ flash rects │ │ flash rects │
└───────────────┘ └───────────────┘ └───────────────┘
Performance Model
Before (O(n) per tick):
Timer tick → compute N colors → store in dict → N widget repaints
After (O(1) per window):
Timer tick → compute colors once → prune expired → ONE overlay.update() per window
Each WindowFlashOverlay.paintEvent() renders all flash rectangles for its window
in a single paint call. Geometry is cached and only recomputed on scroll/resize.
Animation Phases
Flash animations have three phases with configurable durations:
fade_in (100ms): Quick fade-in with OutQuad easing
hold (50ms): Hold at maximum intensity
fade_out (350ms): Slow fade-out with InOutCubic easing
Widget-Type-Specific Masking
Flash animations use widget-type-specific masking strategies for precise visual feedback:
Masking Strategies:
Checkbox: Tight mask for indicator + label text using Qt style subelement rects
Label: Tight mask using
sizeHint()to avoid empty layout spaceHelp Button: Fixed square mask when
_square_sizeis setAll other widgets: Full rectangle mask
Checkbox Square Cutout:
Textless checkboxes (no label) use square cutouts to avoid rounding:
def _needs_square_checkbox_mask(widget: QWidget) -> bool:
return isinstance(widget, QCheckBox) and not widget.text()
Function Pane Title Masking:
Function panes mask title row widgets tightly:
def _get_function_pane_title_widgets(groupbox: QWidget) -> List[QWidget]:
pane = groupbox
while pane is not None:
if hasattr(pane, "_flash_title_container") or hasattr(pane, "_module_path_label"):
break
pane = pane.parentWidget()
widgets = []
module_label = getattr(pane, "_module_path_label", None)
if module_label and module_label.isVisible():
widgets.append(module_label)
title_container = getattr(pane, "_flash_title_container", None)
if title_container and title_container.isVisible():
for child in title_container.findChildren(QWidget):
if child.isVisible() and isinstance(child, LEAF_WIDGET_TYPES):
widgets.append(child)
return widgets
FlashElement Types
The system supports multiple element types via FlashElement dataclass:
Element Type |
Factory Function |
Use Case |
|---|---|---|
Groupbox |
|
Form section headers (STANDARD mode masks all children, INVERSE mode masks title + leaf_widget) |
Groupbox (full rect) |
|
Flash entire groupbox geometry (no margin-top offset) |
Tree Item |
|
Config hierarchy trees |
List Item |
|
Step/function lists |
INVERSE Mode with Label Widget Masking
INVERSE mode now masks title + leaf_widget + label_widget (not all title row widgets):
self.register_flash_leaf(
key="my_field",
groupbox=my_groupbox,
leaf_widget=my_widget,
label_widget=my_label # NEW: mask label too
)
This highlights “all fields that inherited the change” while keeping the changed field and its label visible.
Masking Behavior:
STANDARD mode (
leaf_widget=None): Mask ALL children, flash only frame/backgroundINVERSE mode (
leaf_widget=widget): Mask title + leaf_widget + label_widget, flash frame + all siblings
Usage with FlashMixin
Widgets inherit FlashMixin (alias: VisualUpdateMixin) to participate:
from pyqt_reactive.animation.flash_mixin import FlashMixin
class MyWidget(QWidget, FlashMixin):
def __init__(self):
super().__init__()
self._init_flash_mixin()
def setup_flash(self, groupbox: QGroupBox):
# Register element for flashing
self.register_flash_groupbox("my_key", groupbox)
def trigger_flash(self):
# Trigger flash (global - all windows with this key)
self.queue_flash("my_key")
# Or local flash (this window only)
self.queue_flash_local("my_key")
Scope-Based Flash Keys
Flash keys are automatically scoped to prevent cross-window contamination:
# Key "well_filter" becomes "orchestrator::plate_1::well_filter"
scoped_key = self._get_scoped_flash_key("well_filter")
This ensures flashing step_0 in plate1 window doesn’t flash step_0 in plate2.
OpenGL Acceleration
On systems with OpenGL 3.3+, the system uses WindowFlashOverlayGL for GPU-accelerated
rendering via instanced draw calls. Falls back to QPainter automatically.
See Also
Flash Callback System - How ObjectState triggers flash callbacks
GUI Performance Patterns - Cross-window preview system
AbstractManagerWidget Architecture - AbstractManagerWidget uses FlashMixin