142 lines
4.6 KiB
Python
142 lines
4.6 KiB
Python
# coding:utf-8
|
|
from collections import deque
|
|
from enum import Enum
|
|
from math import cos, pi, ceil
|
|
|
|
from PySide6.QtCore import QDateTime, Qt, QTimer, QPoint
|
|
from PySide6.QtGui import QWheelEvent
|
|
from PySide6.QtWidgets import QApplication, QScrollArea, QAbstractScrollArea
|
|
|
|
|
|
class SmoothScroll:
|
|
""" Scroll smoothly """
|
|
|
|
def __init__(self, widget: QScrollArea, orient=Qt.Vertical):
|
|
"""
|
|
Parameters
|
|
----------
|
|
widget: QScrollArea
|
|
scroll area to scroll smoothly
|
|
|
|
orient: Orientation
|
|
scroll orientation
|
|
"""
|
|
self.widget = widget
|
|
self.orient = orient
|
|
self.fps = 60
|
|
self.duration = 400
|
|
self.stepsTotal = 0
|
|
self.stepRatio = 1.5
|
|
self.acceleration = 1
|
|
self.lastWheelEvent = None
|
|
self.scrollStamps = deque()
|
|
self.stepsLeftQueue = deque()
|
|
self.smoothMoveTimer = QTimer(widget)
|
|
self.smoothMode = SmoothMode(SmoothMode.LINEAR)
|
|
self.smoothMoveTimer.timeout.connect(self.__smoothMove)
|
|
|
|
def setSmoothMode(self, smoothMode):
|
|
""" set smooth mode """
|
|
self.smoothMode = smoothMode
|
|
|
|
def wheelEvent(self, e):
|
|
# only process the wheel events triggered by mouse, fixes issue #75
|
|
delta = e.angleDelta().y() if e.angleDelta().y() != 0 else e.angleDelta().x()
|
|
if self.smoothMode == SmoothMode.NO_SMOOTH or abs(delta) % 120 != 0:
|
|
QAbstractScrollArea.wheelEvent(self.widget, e)
|
|
return
|
|
|
|
# push current time to queque
|
|
now = QDateTime.currentDateTime().toMSecsSinceEpoch()
|
|
self.scrollStamps.append(now)
|
|
while now - self.scrollStamps[0] > 500:
|
|
self.scrollStamps.popleft()
|
|
|
|
# adjust the acceration ratio based on unprocessed events
|
|
accerationRatio = min(len(self.scrollStamps) / 15, 1)
|
|
self.lastWheelPos = e.position()
|
|
self.lastWheelGlobalPos = e.globalPosition()
|
|
|
|
# get the number of steps
|
|
self.stepsTotal = self.fps * self.duration / 1000
|
|
|
|
# get the moving distance corresponding to each event
|
|
delta = delta* self.stepRatio
|
|
if self.acceleration > 0:
|
|
delta += delta * self.acceleration * accerationRatio
|
|
|
|
# form a list of moving distances and steps, and insert it into the queue for processing.
|
|
self.stepsLeftQueue.append([delta, self.stepsTotal])
|
|
|
|
# overflow time of timer: 1000ms/frames
|
|
self.smoothMoveTimer.start(int(1000 / self.fps))
|
|
|
|
def __smoothMove(self):
|
|
""" scroll smoothly when timer time out """
|
|
totalDelta = 0
|
|
|
|
# Calculate the scrolling distance of all unprocessed events,
|
|
# the timer will reduce the number of steps by 1 each time it overflows.
|
|
for i in self.stepsLeftQueue:
|
|
totalDelta += self.__subDelta(i[0], i[1])
|
|
i[1] -= 1
|
|
|
|
# If the event has been processed, move it out of the queue
|
|
while self.stepsLeftQueue and self.stepsLeftQueue[0][1] == 0:
|
|
self.stepsLeftQueue.popleft()
|
|
|
|
# construct wheel event
|
|
if self.orient == Qt.Vertical:
|
|
pixelDelta = QPoint(round(totalDelta), 0)
|
|
bar = self.widget.verticalScrollBar()
|
|
else:
|
|
pixelDelta = QPoint(0, round(totalDelta))
|
|
bar = self.widget.horizontalScrollBar()
|
|
|
|
e = QWheelEvent(
|
|
self.lastWheelPos,
|
|
self.lastWheelGlobalPos,
|
|
pixelDelta,
|
|
QPoint(round(totalDelta), 0),
|
|
Qt.MouseButton.LeftButton,
|
|
Qt.KeyboardModifier.NoModifier,
|
|
Qt.ScrollPhase.ScrollBegin,
|
|
False,
|
|
)
|
|
|
|
# send wheel event to app
|
|
QApplication.sendEvent(bar, e)
|
|
|
|
# stop scrolling if the queque is empty
|
|
if not self.stepsLeftQueue:
|
|
self.smoothMoveTimer.stop()
|
|
|
|
def __subDelta(self, delta, stepsLeft):
|
|
""" get the interpolation for each step """
|
|
m = self.stepsTotal / 2
|
|
x = abs(self.stepsTotal - stepsLeft - m)
|
|
|
|
res = 0
|
|
if self.smoothMode == SmoothMode.NO_SMOOTH:
|
|
res = 0
|
|
elif self.smoothMode == SmoothMode.CONSTANT:
|
|
res = delta / self.stepsTotal
|
|
elif self.smoothMode == SmoothMode.LINEAR:
|
|
res = 2 * delta / self.stepsTotal * (m - x) / m
|
|
elif self.smoothMode == SmoothMode.QUADRATI:
|
|
res = 3 / 4 / m * (1 - x * x / m / m) * delta
|
|
elif self.smoothMode == SmoothMode.COSINE:
|
|
res = (cos(x * pi / m) + 1) / (2 * m) * delta
|
|
|
|
return res
|
|
|
|
|
|
class SmoothMode(Enum):
|
|
""" Smooth mode """
|
|
NO_SMOOTH = 0
|
|
CONSTANT = 1
|
|
LINEAR = 2
|
|
QUADRATI = 3
|
|
COSINE = 4
|
|
|