fix(animations): resolve memory leaks in GameCardAnimations and DetailPageAnimations
All checks were successful
Code check / Check code (push) Successful in 1m48s
All checks were successful
Code check / Check code (push) Successful in 1m48s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -33,6 +33,47 @@ class GameCardAnimations:
|
||||
self.pulse_anim: QPropertyAnimation | None = None
|
||||
self._isPulseAnimationConnected = False
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up all animation objects to prevent memory leaks."""
|
||||
if self.thickness_anim:
|
||||
try:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
try:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
except RuntimeError:
|
||||
pass # Signal was already disconnected
|
||||
self.thickness_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.thickness_anim = None
|
||||
|
||||
if self.gradient_anim:
|
||||
try:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.gradient_anim = None
|
||||
|
||||
if self.scale_anim:
|
||||
try:
|
||||
self.scale_anim.stop()
|
||||
self.scale_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.scale_anim = None
|
||||
|
||||
if self.pulse_anim:
|
||||
try:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.pulse_anim = None
|
||||
|
||||
self._isPulseAnimationConnected = False
|
||||
|
||||
def setup_animations(self):
|
||||
"""Initialize animation properties based on theme."""
|
||||
self.thickness_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
|
||||
@@ -50,8 +91,16 @@ class GameCardAnimations:
|
||||
"""Start pulse animation for border width when hovered or focused."""
|
||||
if not (self.game_card._hovered or self.game_card._focused):
|
||||
return
|
||||
|
||||
# Clean up existing pulse animation to prevent memory leaks
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
try:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.pulse_anim = None
|
||||
|
||||
self.pulse_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
|
||||
self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"])
|
||||
self.pulse_anim.setLoopCount(0)
|
||||
@@ -74,7 +123,10 @@ class GameCardAnimations:
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
try:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
except RuntimeError:
|
||||
pass # Signal was already disconnected
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
@@ -84,8 +136,15 @@ class GameCardAnimations:
|
||||
self.thickness_anim.start()
|
||||
|
||||
if animation_type == "gradient":
|
||||
# Clean up existing gradient animation to prevent memory leaks
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
try:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.gradient_anim = None
|
||||
|
||||
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
||||
@@ -93,8 +152,15 @@ class GameCardAnimations:
|
||||
self.gradient_anim.setLoopCount(-1)
|
||||
self.gradient_anim.start()
|
||||
elif animation_type == "scale":
|
||||
# Clean up existing scale animation to prevent memory leaks
|
||||
if self.scale_anim:
|
||||
self.scale_anim.stop()
|
||||
try:
|
||||
self.scale_anim.stop()
|
||||
self.scale_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.scale_anim = None
|
||||
|
||||
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve"]]))
|
||||
@@ -110,11 +176,21 @@ class GameCardAnimations:
|
||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||
if animation_type == "gradient":
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
try:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.gradient_anim = None
|
||||
elif animation_type == "scale":
|
||||
if self.scale_anim:
|
||||
self.scale_anim.stop()
|
||||
try:
|
||||
self.scale_anim.stop()
|
||||
self.scale_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.scale_anim = None
|
||||
|
||||
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve_out"]]))
|
||||
@@ -122,12 +198,19 @@ class GameCardAnimations:
|
||||
self.scale_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_scale"])
|
||||
self.scale_anim.start()
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
try:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.pulse_anim = None
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
try:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
except RuntimeError:
|
||||
pass # Signal was already disconnected
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
@@ -148,7 +231,10 @@ class GameCardAnimations:
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
try:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
except RuntimeError:
|
||||
pass # Signal was already disconnected
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
@@ -158,8 +244,15 @@ class GameCardAnimations:
|
||||
self.thickness_anim.start()
|
||||
|
||||
if animation_type == "gradient":
|
||||
# Clean up existing gradient animation to prevent memory leaks
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
try:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.gradient_anim = None
|
||||
|
||||
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
||||
@@ -167,8 +260,15 @@ class GameCardAnimations:
|
||||
self.gradient_anim.setLoopCount(-1)
|
||||
self.gradient_anim.start()
|
||||
elif animation_type == "scale":
|
||||
# Clean up existing scale animation to prevent memory leaks
|
||||
if self.scale_anim:
|
||||
self.scale_anim.stop()
|
||||
try:
|
||||
self.scale_anim.stop()
|
||||
self.scale_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.scale_anim = None
|
||||
|
||||
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve"]]))
|
||||
@@ -184,11 +284,21 @@ class GameCardAnimations:
|
||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("card_animation_type", "gradient")
|
||||
if animation_type == "gradient":
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
try:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.gradient_anim = None
|
||||
elif animation_type == "scale":
|
||||
if self.scale_anim:
|
||||
self.scale_anim.stop()
|
||||
try:
|
||||
self.scale_anim.stop()
|
||||
self.scale_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.scale_anim = None
|
||||
|
||||
self.scale_anim = QPropertyAnimation(self.game_card, QByteArray(b"scale"))
|
||||
self.scale_anim.setDuration(self.theme.GAME_CARD_ANIMATION["scale_anim_duration"])
|
||||
self.scale_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["scale_easing_curve_out"]]))
|
||||
@@ -196,12 +306,19 @@ class GameCardAnimations:
|
||||
self.scale_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_scale"])
|
||||
self.scale_anim.start()
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
try:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.pulse_anim = None
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
try:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
except RuntimeError:
|
||||
pass # Signal was already disconnected
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
@@ -242,6 +359,18 @@ class DetailPageAnimations:
|
||||
main_window._animations = {}
|
||||
self.animations = main_window._animations
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up all animations to prevent memory leaks."""
|
||||
# Stop and clean up all animations in the dict
|
||||
for _detail_page, animation in list(self.animations.items()):
|
||||
try:
|
||||
if isinstance(animation, QAbstractAnimation):
|
||||
animation.stop()
|
||||
animation.deleteLater()
|
||||
except RuntimeError:
|
||||
pass # Object already deleted
|
||||
self.animations.clear()
|
||||
|
||||
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."""
|
||||
# Check if the detail page is still valid before proceeding
|
||||
@@ -254,6 +383,20 @@ class DetailPageAnimations:
|
||||
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)
|
||||
|
||||
# Safely stop and remove any existing animation for this detail page
|
||||
if detail_page in self.animations:
|
||||
try:
|
||||
existing_animation = self.animations[detail_page]
|
||||
if isinstance(existing_animation, QAbstractAnimation) and existing_animation.state() == QAbstractAnimation.State.Running:
|
||||
existing_animation.stop()
|
||||
existing_animation.deleteLater()
|
||||
except RuntimeError:
|
||||
logger.debug("Existing animation already deleted")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping existing animation: {e}", exc_info=True)
|
||||
finally:
|
||||
self.animations.pop(detail_page, None)
|
||||
|
||||
if animation_type == "fade":
|
||||
# Check again if page is still valid before starting animation
|
||||
if not detail_page or detail_page.isHidden():
|
||||
@@ -380,6 +523,7 @@ class DetailPageAnimations:
|
||||
animation = self.animations[detail_page]
|
||||
if isinstance(animation, QAbstractAnimation) and animation.state() == QAbstractAnimation.State.Running:
|
||||
animation.stop()
|
||||
animation.deleteLater()
|
||||
except RuntimeError:
|
||||
logger.warning("Animation already deleted for page")
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import shlex
|
||||
from PySide6.QtWidgets import (QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QLabel, QHBoxLayout, QWidget)
|
||||
from PySide6.QtWidgets import (QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QApplication)
|
||||
from PySide6.QtCore import Qt, QUrl, QTimer, QAbstractAnimation
|
||||
from PySide6.QtGui import QColor, QDesktopServices
|
||||
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
||||
@@ -500,7 +500,7 @@ class DetailPageManager:
|
||||
return
|
||||
|
||||
# Process events to ensure UI state is updated
|
||||
self.main_window.processEvents()
|
||||
QApplication.processEvents()
|
||||
self.main_window.activateWindow()
|
||||
self.main_window.stackedWidget.setCurrentWidget(detailPage)
|
||||
detailPage.raise_()
|
||||
@@ -511,7 +511,7 @@ class DetailPageManager:
|
||||
logger.debug("Final retry...")
|
||||
playButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
playButton.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
self.main_window.processEvents()
|
||||
QApplication.processEvents()
|
||||
|
||||
if playButton.hasFocus():
|
||||
logger.debug("Play button received focus after final retry")
|
||||
@@ -728,7 +728,7 @@ class DetailPageManager:
|
||||
return
|
||||
|
||||
# Process events to ensure UI state is updated
|
||||
self.main_window.processEvents()
|
||||
QApplication.processEvents()
|
||||
self.main_window.activateWindow()
|
||||
self.main_window.stackedWidget.setCurrentWidget(detailPage)
|
||||
detailPage.raise_()
|
||||
@@ -739,7 +739,7 @@ class DetailPageManager:
|
||||
logger.debug("Final retry...")
|
||||
installButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
installButton.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
self.main_window.processEvents()
|
||||
QApplication.processEvents()
|
||||
|
||||
if installButton.hasFocus():
|
||||
logger.debug("Install button received focus after final retry")
|
||||
|
||||
@@ -560,6 +560,23 @@ class GameCard(QFrame):
|
||||
)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up animations to prevent memory leaks when the card is destroyed."""
|
||||
if hasattr(self, 'animations') and self.animations:
|
||||
try:
|
||||
self.animations.cleanup()
|
||||
except RuntimeError:
|
||||
# Object already deleted
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
"""Destructor to ensure cleanup happens."""
|
||||
try:
|
||||
self.cleanup()
|
||||
except RuntimeError:
|
||||
# Object already deleted
|
||||
pass
|
||||
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
||||
|
||||
@@ -2984,6 +2984,14 @@ class MainWindow(QMainWindow):
|
||||
timer.deleteLater()
|
||||
setattr(self, tname, None)
|
||||
|
||||
# Clean up animations to prevent memory leaks
|
||||
if hasattr(self, 'detail_animations'):
|
||||
try:
|
||||
self.detail_animations.cleanup()
|
||||
except RuntimeError:
|
||||
# Object already deleted
|
||||
pass
|
||||
|
||||
def _update_card_name_from_metadata(self, exe_name: str, metadata_path: str):
|
||||
"""Update card name from metadata file."""
|
||||
# Read the translated metadata using the existing function
|
||||
|
||||
Reference in New Issue
Block a user