feat: cross-fade and palette flip on theme switch

Freeze the old look in a click-through screenshot overlay and fade it
out over the restyled window — QSS has no transitions and animating the
palette would re-polish every widget per frame. Sunset also flips the
app-palette text roles so native primitives QSS can't recolor (spin and
combo arrow glyphs) follow the theme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 15:07:23 +02:00
co-authored by Claude Fable 5
parent ab65193443
commit fbbb236844
2 changed files with 55 additions and 2 deletions
+52 -2
View File
@@ -15,15 +15,25 @@ from aarecommon.models.models import (
SessionsStateEnum,
TokenData,
)
from PySide6.QtCore import QEvent, QSettings, Qt, QTimer, Signal, Slot
from PySide6.QtGui import QAction, QActionGroup, QColor, QCursor, QGuiApplication, QKeySequence
from PySide6.QtCore import QEvent, QPropertyAnimation, QSettings, Qt, QTimer, Signal, Slot
from PySide6.QtGui import (
QAction,
QActionGroup,
QColor,
QCursor,
QGuiApplication,
QKeySequence,
QPalette,
)
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QDockWidget,
QFrame,
QGraphicsColorizeEffect,
QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QMainWindow,
QMessageBox,
QPushButton,
@@ -72,10 +82,13 @@ from aare.gui.scan_logic.rotation_scan_manager import RotationScanManager
from aare.gui.scan_logic.sample_mount_logic import SampleMountLogic
from aare.gui.styles import (
APP_BACKGROUND,
DARK_TEXT,
DOCK_CONTENT_LEFT_PAD,
THEME_FADE_MS,
THEME_ORIGINAL,
THEME_PORTRAIT,
build_app_stylesheet,
qcolor,
)
# Threads
@@ -1682,9 +1695,46 @@ class MainWindow(QMainWindow):
self.tutorial_manager.start("manual_workflow_demo")
def _apply_theme(self) -> None:
# Screenshot cross-fade (THEME_FADE_MS in styles.py): freeze the old
# look in a click-through overlay, restyle beneath it in one normal
# repolish, fade the overlay out. QSS has no transitions, and
# animating the palette would re-polish every widget per frame.
old_look = self.grab() if self.isVisible() else None
# Native primitives QSS can't recolor (spin/combo arrow glyphs) draw
# from the app palette — flip its text roles with the theme.
app = QApplication.instance()
if not hasattr(self, "_default_palette"):
self._default_palette = app.palette()
if self._theme_mode == THEME_PORTRAIT:
palette = QPalette(self._default_palette)
for role in (
QPalette.ColorRole.ButtonText,
QPalette.ColorRole.Text,
QPalette.ColorRole.WindowText,
):
palette.setColor(role, qcolor(DARK_TEXT))
app.setPalette(palette)
else:
app.setPalette(self._default_palette)
self.setStyleSheet(build_app_stylesheet(self._theme_mode))
# State colors are painted in code per DAQ tick — QSS can't reach them.
self.beamline_state_panel.set_theme(self._theme_mode)
if old_look is None:
return
overlay = QLabel(self)
overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
overlay.setPixmap(old_look)
overlay.setGeometry(self.rect())
overlay.raise_()
overlay.show()
effect = QGraphicsOpacityEffect(overlay)
overlay.setGraphicsEffect(effect)
fade = QPropertyAnimation(effect, b"opacity", overlay)
fade.setDuration(THEME_FADE_MS)
fade.setStartValue(1.0)
fade.setEndValue(0.0)
fade.finished.connect(overlay.deleteLater)
fade.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
def _restore_theme_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
+3
View File
@@ -99,6 +99,9 @@ DOCK_CONTENT_LEFT_PAD = 10
# lives in MainWindow.event(); the QSS :hover part picks the one separator.
SEPARATOR_HINT = "rgba(168, 178, 192, 50%)" # scrollbar-track grey @50%
# Theme-switch screenshot cross-fade duration (int ms, used in code).
THEME_FADE_MS = 250
# Tab face fill: the selected Dewar/Auxiliary tab AND the unchecked
# All/Queued/Flagged/Measured filter buttons share this background.
TAB_FACE_BG = "#ffffff"