From b070ff1fca20a545da71aa08a7bbc45854e00d96 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Thu, 14 Aug 2025 13:13:23 +0500 Subject: [PATCH] fix(animations): fix all Qpainter conflicts Signed-off-by: Boris Yumankulov --- portprotonqt/animations.py | 53 +++++++++++++++++++++++++++++++------- portprotonqt/egs_api.py | 2 -- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/portprotonqt/animations.py b/portprotonqt/animations.py index 2b8c7d0..84a1828 100644 --- a/portprotonqt/animations.py +++ b/portprotonqt/animations.py @@ -7,6 +7,19 @@ from portprotonqt.logger import get_logger logger = get_logger(__name__) +class SafeOpacityEffect(QGraphicsOpacityEffect): + def __init__(self, parent=None, disable_at_full=True): + super().__init__(parent) + self.disable_at_full = disable_at_full + + def setOpacity(self, opacity: float): + opacity = max(0.0, min(1.0, opacity)) + super().setOpacity(opacity) + if opacity < 1.0: + self.setEnabled(True) + elif self.disable_at_full: + self.setEnabled(False) + class GameCardAnimations: def __init__(self, game_card, theme=None): self.game_card = game_card @@ -138,7 +151,9 @@ class GameCardAnimations: self.thickness_anim.start() def paint_border(self, painter: QPainter): - """Paint the animated border for the GameCard.""" + if not painter.isActive(): + logger.warning("Painter is not active; skipping border paint") + return painter.setRenderHint(QPainter.RenderHint.Antialiasing) pen = QPen() pen.setWidth(self.game_card._borderWidth) @@ -154,6 +169,8 @@ class GameCardAnimations: radius = 18 bw = round(self.game_card._borderWidth / 2) rect = self.game_card.rect().adjusted(bw, bw, -bw, -bw) + if rect.isEmpty(): + return # Avoid drawing invalid rect painter.drawRoundedRect(rect, radius, radius) class DetailPageAnimations: @@ -164,21 +181,28 @@ class DetailPageAnimations: def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable): """Animate the detail page based on theme settings.""" - shadow = detail_page.graphicsEffect() animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade") duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350) if animation_type == "fade": - opacity_effect = QGraphicsOpacityEffect(detail_page) + original_effect = detail_page.graphicsEffect() + opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=True) + opacity_effect.setOpacity(0.0) detail_page.setGraphicsEffect(opacity_effect) animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity")) animation.setDuration(duration) - animation.setStartValue(0) - animation.setEndValue(1) + animation.setStartValue(0.0) + animation.setEndValue(0.999) animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) self.animations[detail_page] = animation - animation.finished.connect(lambda: detail_page.setGraphicsEffect(shadow) if shadow is not None else detail_page.setGraphicsEffect(None)) # type: ignore + def restore_effect(): + try: + detail_page.setGraphicsEffect(original_effect) # type: ignore + except RuntimeError: + logger.debug("Original effect already deleted") + animation.finished.connect(restore_effect) animation.finished.connect(load_image_and_restore_effect) + animation.finished.connect(opacity_effect.deleteLater) elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]: duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500) easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")]) @@ -243,15 +267,24 @@ class DetailPageAnimations: # Define animation based on type if animation_type == "fade": duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration_exit", 350) - opacity_effect = QGraphicsOpacityEffect(detail_page) + original_effect = detail_page.graphicsEffect() + opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=False) + opacity_effect.setOpacity(0.999) detail_page.setGraphicsEffect(opacity_effect) animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity")) animation.setDuration(duration) - animation.setStartValue(1) - animation.setEndValue(0) + animation.setStartValue(0.999) + animation.setEndValue(0.0) animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) self.animations[detail_page] = animation - animation.finished.connect(cleanup_callback) + def restore_and_cleanup(): + try: + detail_page.setGraphicsEffect(original_effect) # type: ignore + except RuntimeError: + logger.debug("Original effect already deleted") + cleanup_callback() + animation.finished.connect(restore_and_cleanup) + animation.finished.connect(opacity_effect.deleteLater) # Clean up effect elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]: duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500) easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")]) diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py index 5e1e1f1..9ca38cd 100644 --- a/portprotonqt/egs_api.py +++ b/portprotonqt/egs_api.py @@ -237,8 +237,6 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba legendary_path: Path to the Legendary CLI executable. callback: Callback function to handle the result (success, message). """ - from portprotonqt.steam_api import call_steam_api # Import for CEF API - if not app_name or not app_name.strip() or not game_title or not game_title.strip(): logger.error("Invalid app_name or game_title: empty or whitespace") callback((False, "Game name or app name is empty or invalid"))