Files
PortProtonQt/portprotonqt/animations.py
Boris Yumankulov 2753e53a4d
All checks were successful
Code check / Check code (push) Successful in 1m50s
refactor: move animations to separate module
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-08-07 10:29:13 +05:00

251 lines
14 KiB
Python

from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
from collections.abc import Callable
import portprotonqt.themes.standart.styles as default_styles
class GameCardAnimations:
def __init__(self, game_card, theme=None):
self.game_card = game_card
self.theme = theme if theme is not None else default_styles
self.thickness_anim: QPropertyAnimation | None = None
self.gradient_anim: QPropertyAnimation | None = None
self.pulse_anim: QPropertyAnimation | None = None
self._isPulseAnimationConnected = False
def setup_animations(self):
"""Initialize animation properties."""
self.thickness_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
def start_pulse_animation(self):
"""Start pulse animation for border width when hovered or focused."""
if not (self.game_card._hovered or self.game_card._focused):
return
if self.pulse_anim:
self.pulse_anim.stop()
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)
self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.start()
def handle_enter_event(self):
"""Handle mouse enter event animations."""
self.game_card._hovered = True
self.game_card.hoverChanged.emit(self.game_card.name, True)
self.game_card.setFocus(Qt.FocusReason.MouseFocusReason)
# Ensure thickness_anim is initialized
if not self.thickness_anim:
self.setup_animations()
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
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)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"])
self.thickness_anim.finished.connect(self.start_pulse_animation)
self._isPulseAnimationConnected = True
self.thickness_anim.start()
if self.gradient_anim:
self.gradient_anim.stop()
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"])
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start()
def handle_leave_event(self):
"""Handle mouse leave event animations."""
self.game_card._hovered = False
self.game_card.hoverChanged.emit(self.game_card.name, False)
if not self.game_card._focused:
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = None
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
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)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
self.thickness_anim.start()
def handle_focus_in_event(self):
"""Handle focus in event animations."""
if not self.game_card._hovered:
self.game_card._focused = True
self.game_card.focusChanged.emit(self.game_card.name, True)
# Ensure thickness_anim is initialized
if not self.thickness_anim:
self.setup_animations()
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
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)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
self.thickness_anim.finished.connect(self.start_pulse_animation)
self._isPulseAnimationConnected = True
self.thickness_anim.start()
if self.gradient_anim:
self.gradient_anim.stop()
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"])
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start()
def handle_focus_out_event(self):
"""Handle focus out event animations."""
self.game_card._focused = False
self.game_card.focusChanged.emit(self.game_card.name, False)
if not self.game_card._hovered:
if self.gradient_anim:
self.gradient_anim.stop()
self.gradient_anim = None
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
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)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
self.thickness_anim.start()
def paint_border(self, painter: QPainter):
"""Paint the animated border for the GameCard."""
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
pen = QPen()
pen.setWidth(self.game_card._borderWidth)
if self.game_card._hovered or self.game_card._focused:
center = self.game_card.rect().center()
gradient = QConicalGradient(center, self.game_card._gradientAngle)
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
gradient.setColorAt(stop["position"], QColor(stop["color"]))
pen.setBrush(QBrush(gradient))
else:
pen.setColor(QColor(0, 0, 0, 0))
painter.setPen(pen)
radius = 18
bw = round(self.game_card._borderWidth / 2)
rect = self.game_card.rect().adjusted(bw, bw, -bw, -bw)
painter.drawRoundedRect(rect, radius, radius)
class DetailPageAnimations:
def __init__(self, main_window, theme=None):
self.main_window = main_window
self.theme = theme if theme is not None else default_styles
self.animations = main_window._animations if hasattr(main_window, '_animations') else {}
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)
detail_page.setGraphicsEffect(opacity_effect)
animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity"))
animation.setDuration(duration)
animation.setStartValue(0)
animation.setEndValue(1)
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
animation.finished.connect(load_image_and_restore_effect)
elif animation_type == "slide_left":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detail_page.move(self.main_window.width(), 0)
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detail_page.pos())
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = animation
animation.finished.connect(cleanup_animation)
animation.finished.connect(load_image_and_restore_effect)
elif animation_type == "slide_right":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detail_page.move(-self.main_window.width(), 0)
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detail_page.pos())
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = animation
animation.finished.connect(cleanup_animation)
animation.finished.connect(load_image_and_restore_effect)
elif animation_type == "slide_up":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detail_page.move(0, self.main_window.height())
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detail_page.pos())
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = animation
animation.finished.connect(cleanup_animation)
animation.finished.connect(load_image_and_restore_effect)
elif animation_type == "slide_down":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
detail_page.move(0, -self.main_window.height())
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
animation.setDuration(duration)
animation.setStartValue(detail_page.pos())
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = animation
animation.finished.connect(cleanup_animation)
animation.finished.connect(load_image_and_restore_effect)
elif animation_type == "bounce":
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_zoom_duration", 400)
detail_page.setWindowOpacity(0.0)
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
opacity_anim.setDuration(duration)
opacity_anim.setStartValue(0.0)
opacity_anim.setEndValue(1.0)
initial_rect = QRect(detail_page.x() + detail_page.width() // 4, detail_page.y() + detail_page.height() // 4,
detail_page.width() // 2, detail_page.height() // 2)
final_rect = detail_page.geometry()
geometry_anim = QPropertyAnimation(detail_page, QByteArray(b"geometry"))
geometry_anim.setDuration(duration)
geometry_anim.setStartValue(initial_rect)
geometry_anim.setEndValue(final_rect)
geometry_anim.setEasingCurve(QEasingCurve.Type.OutBack)
group_anim = QParallelAnimationGroup()
group_anim.addAnimation(opacity_anim)
group_anim.addAnimation(geometry_anim)
group_anim.finished.connect(load_image_and_restore_effect)
group_anim.finished.connect(cleanup_animation)
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = group_anim