Files
fluent_widgets_pyside6/qfluentwidgets/common/animation.py
2025-08-14 18:45:16 +08:00

530 lines
14 KiB
Python

# coding: utf-8
from enum import Enum
from PySide6.QtCore import QEasingCurve, QEvent, QObject, QPropertyAnimation, Property, Signal, QPoint, QPointF
from PySide6.QtGui import QMouseEvent, QEnterEvent, QColor
from PySide6.QtWidgets import QWidget, QLineEdit, QGraphicsDropShadowEffect
from .config import qconfig
class AnimationBase(QObject):
""" Animation base class """
def __init__(self, parent: QWidget):
super().__init__(parent=parent)
parent.installEventFilter(self)
def _onHover(self, e: QEnterEvent):
pass
def _onLeave(self, e: QEvent):
pass
def _onPress(self, e: QMouseEvent):
pass
def _onRelease(self, e: QMouseEvent):
pass
def eventFilter(self, obj, e: QEvent):
if obj is self.parent():
if e.type() == QEvent.MouseButtonPress:
self._onPress(e)
elif e.type() == QEvent.MouseButtonRelease:
self._onRelease(e)
elif e.type() == QEvent.Enter:
self._onHover(e)
elif e.type() == QEvent.Leave:
self._onLeave(e)
return super().eventFilter(obj, e)
class TranslateYAnimation(AnimationBase):
valueChanged = Signal(float)
def __init__(self, parent: QWidget, offset=2):
super().__init__(parent)
self._y = 0
self.maxOffset = offset
self.ani = QPropertyAnimation(self, b'y', self)
def getY(self):
return self._y
def setY(self, y):
self._y = y
self.parent().update()
self.valueChanged.emit(y)
def _onPress(self, e):
""" arrow down """
self.ani.setEndValue(self.maxOffset)
self.ani.setEasingCurve(QEasingCurve.OutQuad)
self.ani.setDuration(150)
self.ani.start()
def _onRelease(self, e):
""" arrow up """
self.ani.setEndValue(0)
self.ani.setDuration(500)
self.ani.setEasingCurve(QEasingCurve.OutElastic)
self.ani.start()
y = Property(float, getY, setY)
class BackgroundAnimationWidget:
""" Background animation widget """
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.isHover = False
self.isPressed = False
self.bgColorObject = BackgroundColorObject(self)
self.backgroundColorAni = QPropertyAnimation(
self.bgColorObject, b'backgroundColor', self)
self.backgroundColorAni.setDuration(120)
self.installEventFilter(self)
qconfig.themeChanged.connect(self._updateBackgroundColor)
def eventFilter(self, obj, e):
if obj is self:
if e.type() == QEvent.Type.EnabledChange:
if self.isEnabled():
self.setBackgroundColor(self._normalBackgroundColor())
else:
self.setBackgroundColor(self._disabledBackgroundColor())
return super().eventFilter(obj, e)
def mousePressEvent(self, e):
self.isPressed = True
self._updateBackgroundColor()
super().mousePressEvent(e)
def mouseReleaseEvent(self, e):
self.isPressed = False
self._updateBackgroundColor()
super().mouseReleaseEvent(e)
def enterEvent(self, e):
self.isHover = True
self._updateBackgroundColor()
def leaveEvent(self, e):
self.isHover = False
self._updateBackgroundColor()
def focusInEvent(self, e):
super().focusInEvent(e)
self._updateBackgroundColor()
def _normalBackgroundColor(self):
return QColor(0, 0, 0, 0)
def _hoverBackgroundColor(self):
return self._normalBackgroundColor()
def _pressedBackgroundColor(self):
return self._normalBackgroundColor()
def _focusInBackgroundColor(self):
return self._normalBackgroundColor()
def _disabledBackgroundColor(self):
return self._normalBackgroundColor()
def _updateBackgroundColor(self):
if not self.isEnabled():
color = self._disabledBackgroundColor()
elif isinstance(self, QLineEdit) and self.hasFocus():
color = self._focusInBackgroundColor()
elif self.isPressed:
color = self._pressedBackgroundColor()
elif self.isHover:
color = self._hoverBackgroundColor()
else:
color = self._normalBackgroundColor()
self.backgroundColorAni.stop()
self.backgroundColorAni.setEndValue(color)
self.backgroundColorAni.start()
def getBackgroundColor(self):
return self.bgColorObject.backgroundColor
def setBackgroundColor(self, color: QColor):
self.bgColorObject.backgroundColor = color
@property
def backgroundColor(self):
return self.getBackgroundColor()
class BackgroundColorObject(QObject):
""" Background color object """
def __init__(self, parent: BackgroundAnimationWidget):
super().__init__(parent)
self._backgroundColor = parent._normalBackgroundColor()
@Property(QColor)
def backgroundColor(self):
return self._backgroundColor
@backgroundColor.setter
def backgroundColor(self, color: QColor):
self._backgroundColor = color
self.parent().update()
class DropShadowAnimation(QPropertyAnimation):
""" Drop shadow animation """
def __init__(self, parent: QWidget, normalColor=QColor(0, 0, 0, 0), hoverColor=QColor(0, 0, 0, 75)):
super().__init__(parent=parent)
self.normalColor = normalColor
self.hoverColor = hoverColor
self.offset = QPoint(0, 0)
self.blurRadius = 38
self.isHover = False
self.shadowEffect = QGraphicsDropShadowEffect(self)
self.shadowEffect.setColor(self.normalColor)
parent.installEventFilter(self)
def setBlurRadius(self, radius: int):
self.blurRadius = radius
def setOffset(self, dx: int, dy: int):
self.offset = QPoint(dx, dy)
def setNormalColor(self, color: QColor):
self.normalColor = color
def setHoverColor(self, color: QColor):
self.hoverColor = color
def setColor(self, color):
pass
def _createShadowEffect(self):
self.shadowEffect = QGraphicsDropShadowEffect(self)
self.shadowEffect.setOffset(self.offset)
self.shadowEffect.setBlurRadius(self.blurRadius)
self.shadowEffect.setColor(self.normalColor)
self.setTargetObject(self.shadowEffect)
self.setStartValue(self.shadowEffect.color())
self.setPropertyName(b'color')
self.setDuration(150)
return self.shadowEffect
def eventFilter(self, obj, e):
if obj is self.parent() and self.parent().isEnabled():
if e.type() in [QEvent.Type.Enter]:
self.isHover = True
if self.state() != QPropertyAnimation.State.Running:
self.parent().setGraphicsEffect(self._createShadowEffect())
self.setEndValue(self.hoverColor)
self.start()
elif e.type() in [QEvent.Type.Leave, QEvent.Type.MouseButtonPress]:
self.isHover = False
if self.parent().graphicsEffect():
self.finished.connect(self._onAniFinished)
self.setEndValue(self.normalColor)
self.start()
return super().eventFilter(obj, e)
def _onAniFinished(self):
self.finished.disconnect()
self.shadowEffect = None
self.parent().setGraphicsEffect(None)
class FluentAnimationSpeed(Enum):
""" Fluent animation speed """
FAST = 0
MEDIUM = 1
SLOW = 2
class FluentAnimationType(Enum):
""" Fluent animation type """
FAST_INVOKE = 0
STRONG_INVOKE = 1
FAST_DISMISS = 2
SOFT_DISMISS = 3
POINT_TO_POINT = 4
FADE_IN_OUT = 5
class FluentAnimationProperty(Enum):
""" Fluent animation property """
POSITION = "position"
SCALE = "scale"
ANGLE = "angle"
OPACITY = "opacity"
class FluentAnimationProperObject(QObject):
""" Fluent animation property object """
objects = {}
def __init__(self, parent=None):
super().__init__(parent=parent)
def getValue(self):
return 0
def setValue(self):
pass
@classmethod
def register(cls, name):
""" register menu animation manager
Parameters
----------
name: Any
the name of manager, it should be unique
"""
def wrapper(Manager):
if name not in cls.objects:
cls.objects[name] = Manager
return Manager
return wrapper
@classmethod
def create(cls, propertyType: FluentAnimationProperty, parent=None):
if propertyType not in cls.objects:
raise ValueError(f"`{propertyType}` has not been registered")
return cls.objects[propertyType](parent)
@FluentAnimationProperObject.register(FluentAnimationProperty.POSITION)
class PositionObject(FluentAnimationProperObject):
""" Position object """
def __init__(self, parent=None):
super().__init__(parent)
self._position = QPoint()
def getValue(self):
return self._position
def setValue(self, pos: QPoint):
self._position = pos
self.parent().update()
position = Property(QPoint, getValue, setValue)
@FluentAnimationProperObject.register(FluentAnimationProperty.SCALE)
class ScaleObject(FluentAnimationProperObject):
""" Scale object """
def __init__(self, parent=None):
super().__init__(parent)
self._scale = 1
def getValue(self):
return self._scale
def setValue(self, scale: float):
self._scale = scale
self.parent().update()
scale = Property(float, getValue, setValue)
@FluentAnimationProperObject.register(FluentAnimationProperty.ANGLE)
class AngleObject(FluentAnimationProperObject):
""" Angle object """
def __init__(self, parent=None):
super().__init__(parent)
self._angle = 0
def getValue(self):
return self._angle
def setValue(self, angle: float):
self._angle = angle
self.parent().update()
angle = Property(float, getValue, setValue)
@FluentAnimationProperObject.register(FluentAnimationProperty.OPACITY)
class OpacityObject(FluentAnimationProperObject):
""" Opacity object """
def __init__(self, parent=None):
super().__init__(parent)
self._opacity = 0
def getValue(self):
return self._opacity
def setValue(self, opacity: float):
self._opacity = opacity
self.parent().update()
opacity = Property(float, getValue, setValue)
class FluentAnimation(QPropertyAnimation):
""" Fluent animation base """
animations = {}
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setSpeed(FluentAnimationSpeed.FAST)
self.setEasingCurve(self.curve())
@classmethod
def createBezierCurve(cls, x1, y1, x2, y2):
curve = QEasingCurve(QEasingCurve.BezierSpline)
curve.addCubicBezierSegment(QPointF(x1, y1), QPointF(x2, y2), QPointF(1, 1))
return curve
@classmethod
def curve(cls):
return cls.createBezierCurve(0, 0, 1, 1)
def setSpeed(self, speed: FluentAnimationSpeed):
""" set the speed of animation """
self.setDuration(self.speedToDuration(speed))
def speedToDuration(self, speed: FluentAnimationSpeed):
return 100
def startAnimation(self, endValue, startValue=None):
self.stop()
if startValue is None:
self.setStartValue(self.value())
else:
self.setStartValue(startValue)
self.setEndValue(endValue)
self.start()
def value(self):
return self.targetObject().getValue()
def setValue(self, value):
self.targetObject().setValue(value)
@classmethod
def register(cls, name):
""" register menu animation manager
Parameters
----------
name: Any
the name of manager, it should be unique
"""
def wrapper(Manager):
if name not in cls.animations:
cls.animations[name] = Manager
return Manager
return wrapper
@classmethod
def create(cls, aniType: FluentAnimationType, propertyType: FluentAnimationProperty,
speed=FluentAnimationSpeed.FAST, value=None, parent=None) -> "FluentAnimation":
if aniType not in cls.animations:
raise ValueError(f"`{aniType}` has not been registered.")
obj = FluentAnimationProperObject.create(propertyType, parent)
ani = cls.animations[aniType](parent)
ani.setSpeed(speed)
ani.setTargetObject(obj)
ani.setPropertyName(propertyType.value.encode())
if value is not None:
ani.setValue(value)
return ani
@FluentAnimation.register(FluentAnimationType.FAST_INVOKE)
class FastInvokeAnimation(FluentAnimation):
""" Fast invoke animation """
@classmethod
def curve(cls):
return cls.createBezierCurve(0, 0, 0, 1)
def speedToDuration(self, speed: FluentAnimationSpeed):
if speed == FluentAnimationSpeed.FAST:
return 187
if speed == FluentAnimationSpeed.MEDIUM:
return 333
return 500
@FluentAnimation.register(FluentAnimationType.STRONG_INVOKE)
class StrongInvokeAnimation(FluentAnimation):
""" Strong invoke animation """
@classmethod
def curve(cls):
return cls.createBezierCurve(0.13, 1.62, 0, 0.92)
def speedToDuration(self, speed: FluentAnimationSpeed):
return 667
@FluentAnimation.register(FluentAnimationType.FAST_DISMISS)
class FastDismissAnimation(FastInvokeAnimation):
""" Fast dismiss animation """
@FluentAnimation.register(FluentAnimationType.SOFT_DISMISS)
class SoftDismissAnimation(FluentAnimation):
""" Soft dismiss animation """
@classmethod
def curve(cls):
return cls.createBezierCurve(1, 0, 1, 1)
def speedToDuration(self, speed: FluentAnimationSpeed):
return 167
@FluentAnimation.register(FluentAnimationType.POINT_TO_POINT)
class PointToPointAnimation(FastDismissAnimation):
""" Point to point animation """
@classmethod
def curve(cls):
return cls.createBezierCurve(0.55, 0.55, 0, 1)
@FluentAnimation.register(FluentAnimationType.FADE_IN_OUT)
class FadeInOutAnimation(FluentAnimation):
""" Fade in/out animation """
def speedToDuration(self, speed: FluentAnimationSpeed):
return 83