From fbbb23684485d3d7fe9d0a92f2b7ca3fa96f2b86 Mon Sep 17 00:00:00 2001 From: Dawn Date: Sat, 8 Aug 2026 15:07:23 +0200 Subject: [PATCH] feat: cross-fade and palette flip on theme switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/aare/gui/main_window.py | 54 +++++++++++++++++++++++++++++++++++-- src/aare/gui/styles.py | 3 +++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 2b15e032..516d680c 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -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") diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py index 405cebf2..fb1cd382 100644 --- a/src/aare/gui/styles.py +++ b/src/aare/gui/styles.py @@ -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"