Batch of performance and animation work: shared FadeHelper/MotionPrefs primitives, hover animators, shadow pixmap caching, smooth scrolling, seek time bubble, and widespread component repaint reductions.
348 lines
11 KiB
C++
348 lines
11 KiB
C++
#include "serverwheelview.h"
|
|
#include <QVariantAnimation>
|
|
#include <QEasingCurve>
|
|
#include <QGraphicsOpacityEffect>
|
|
#include <QMouseEvent>
|
|
#include <QWheelEvent>
|
|
#include <QDateTime>
|
|
#include <QMap>
|
|
#include <QtMath>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QFont>
|
|
#include "../tokens/AnimationTokens.h"
|
|
#include "../utils/motionprefs.h"
|
|
|
|
ServerWheelView::ServerWheelView(QWidget* parent) : QWidget(parent) {
|
|
setAttribute(Qt::WA_StyledBackground, true);
|
|
setStyleSheet("ServerWheelView { background: transparent; }");
|
|
|
|
m_scrollAnim = new QVariantAnimation(this);
|
|
|
|
m_scrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
m_scrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
|
connect(m_scrollAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant& val){
|
|
m_scrollOffset = val.toReal();
|
|
updateLayout();
|
|
});
|
|
}
|
|
|
|
ServerWheelView::~ServerWheelView() {
|
|
clear();
|
|
}
|
|
|
|
void ServerWheelView::addCard(QWidget* card) {
|
|
card->setParent(this);
|
|
auto* effect = new QGraphicsOpacityEffect(card);
|
|
card->setGraphicsEffect(effect);
|
|
|
|
installFilterRecursively(card);
|
|
m_items.append(card);
|
|
|
|
updateLayout();
|
|
}
|
|
|
|
void ServerWheelView::clear() {
|
|
m_scrollAnim->stop();
|
|
m_scrollOffset = 0.0;
|
|
m_currentIndex = 0;
|
|
qDeleteAll(m_items);
|
|
m_items.clear();
|
|
}
|
|
|
|
int ServerWheelView::count() const {
|
|
return m_items.size();
|
|
}
|
|
|
|
void ServerWheelView::setCurrentIndex(int index, bool animated) {
|
|
int N = m_items.size();
|
|
if (N == 0) return;
|
|
|
|
index = index % N;
|
|
if (index < 0) index += N;
|
|
|
|
if (animated) {
|
|
qreal delta = index - m_scrollOffset;
|
|
qreal wrapped_delta = delta - N * qRound(delta / static_cast<qreal>(N));
|
|
scrollTo(m_scrollOffset + wrapped_delta);
|
|
} else {
|
|
m_scrollAnim->stop();
|
|
m_scrollOffset = index;
|
|
updateLayout();
|
|
}
|
|
}
|
|
|
|
int ServerWheelView::currentIndex() const {
|
|
return m_currentIndex;
|
|
}
|
|
|
|
void ServerWheelView::setTransitionMode(bool active) {
|
|
m_isTransitioning = active;
|
|
if (active) {
|
|
m_scrollAnim->stop();
|
|
m_scrollOffset = m_currentIndex;
|
|
}
|
|
updateLayout();
|
|
}
|
|
|
|
void ServerWheelView::resizeEvent(QResizeEvent*) {
|
|
updateLayout();
|
|
}
|
|
|
|
void ServerWheelView::handleMousePress(QPoint globalPos) {
|
|
if (m_items.isEmpty()) return;
|
|
m_scrollAnim->stop();
|
|
m_isDragging = true;
|
|
m_dragStarted = false;
|
|
m_lastPos = globalPos;
|
|
m_dragVelocity = 0.0;
|
|
m_lastTime = QDateTime::currentMSecsSinceEpoch();
|
|
}
|
|
|
|
void ServerWheelView::handleMouseMove(QPoint globalPos) {
|
|
if (!m_isDragging) return;
|
|
qreal dy = globalPos.y() - m_lastPos.y();
|
|
if (!m_dragStarted && qAbs(dy) > 5) m_dragStarted = true;
|
|
if (m_dragStarted) {
|
|
qreal deltaOffset = -dy / 80.0;
|
|
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
qint64 dt = now - m_lastTime;
|
|
if (dt > 0) m_dragVelocity = deltaOffset / dt * 16.0;
|
|
|
|
m_scrollOffset += deltaOffset;
|
|
|
|
m_lastPos = globalPos;
|
|
m_lastTime = now;
|
|
updateLayout();
|
|
}
|
|
}
|
|
|
|
bool ServerWheelView::handleMouseRelease() {
|
|
if (!m_isDragging) return false;
|
|
m_isDragging = false;
|
|
if (m_dragStarted) {
|
|
if (!m_items.isEmpty()) {
|
|
qreal target = qRound(m_scrollOffset + m_dragVelocity * 10.0);
|
|
scrollTo(target);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ServerWheelView::mousePressEvent(QMouseEvent* event) {
|
|
if (event->button() == Qt::LeftButton) handleMousePress(event->globalPosition().toPoint());
|
|
QWidget::mousePressEvent(event);
|
|
}
|
|
|
|
void ServerWheelView::mouseMoveEvent(QMouseEvent* event) {
|
|
handleMouseMove(event->globalPosition().toPoint());
|
|
QWidget::mouseMoveEvent(event);
|
|
}
|
|
|
|
void ServerWheelView::mouseReleaseEvent(QMouseEvent* event) {
|
|
if (event->button() == Qt::LeftButton) handleMouseRelease();
|
|
QWidget::mouseReleaseEvent(event);
|
|
}
|
|
|
|
bool ServerWheelView::eventFilter(QObject* obj, QEvent* event) {
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
if (me->button() == Qt::LeftButton) handleMousePress(me->globalPosition().toPoint());
|
|
}
|
|
else if (event->type() == QEvent::MouseMove) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
handleMouseMove(me->globalPosition().toPoint());
|
|
if (m_dragStarted) return true;
|
|
}
|
|
else if (event->type() == QEvent::MouseButtonRelease) {
|
|
auto* me = static_cast<QMouseEvent*>(event);
|
|
if (me->button() == Qt::LeftButton) {
|
|
if (handleMouseRelease()) return true;
|
|
}
|
|
}
|
|
return QWidget::eventFilter(obj, event);
|
|
}
|
|
|
|
void ServerWheelView::wheelEvent(QWheelEvent* event) {
|
|
if (m_items.isEmpty()) return;
|
|
|
|
|
|
|
|
qreal angleDelta = event->angleDelta().y();
|
|
qreal step = -angleDelta / 120.0;
|
|
|
|
|
|
qreal baseTarget = m_scrollAnim->state() == QAbstractAnimation::Running ?
|
|
m_scrollAnim->endValue().toReal() : qRound(m_scrollOffset);
|
|
|
|
scrollTo(baseTarget + step);
|
|
}
|
|
|
|
void ServerWheelView::installFilterRecursively(QWidget* w) {
|
|
w->installEventFilter(this);
|
|
for (auto* child : w->findChildren<QWidget*>()) {
|
|
child->installEventFilter(this);
|
|
}
|
|
}
|
|
|
|
void ServerWheelView::scrollTo(qreal target) {
|
|
m_scrollAnim->stop();
|
|
if (MotionPrefs::reduceMotion()) {
|
|
m_scrollOffset = target;
|
|
updateLayout();
|
|
return;
|
|
}
|
|
m_scrollAnim->setStartValue(m_scrollOffset);
|
|
m_scrollAnim->setEndValue(target);
|
|
m_scrollAnim->start();
|
|
}
|
|
|
|
void ServerWheelView::updateLayout() {
|
|
int N = m_items.size();
|
|
if (N == 0) return;
|
|
|
|
int vh = height();
|
|
int vw = width();
|
|
int centerY = vh / 2;
|
|
int baseItemHeight = 72;
|
|
|
|
QMap<qreal, QWidget*> zOrderMap;
|
|
|
|
int newIndex = qRound(m_scrollOffset) % N;
|
|
if (newIndex < 0) newIndex += N;
|
|
|
|
if (newIndex != m_currentIndex) {
|
|
m_currentIndex = newIndex;
|
|
Q_EMIT currentIndexChanged(m_currentIndex);
|
|
}
|
|
|
|
for (int i = 0; i < N; ++i) {
|
|
QWidget* card = m_items[i];
|
|
|
|
qreal rawDelta = i - m_scrollOffset;
|
|
qreal logicalIdx = rawDelta - N * qRound(rawDelta / static_cast<qreal>(N));
|
|
qreal distance = qAbs(logicalIdx);
|
|
|
|
qreal opacity = qMax(0.0, 1.0 - distance * 0.4);
|
|
bool shouldHide = (opacity <= 0.0 || (m_isTransitioning && i != m_currentIndex));
|
|
|
|
|
|
if (shouldHide) {
|
|
if (card->isVisible()) card->hide();
|
|
continue;
|
|
} else {
|
|
if (!card->isVisible()) card->show();
|
|
}
|
|
|
|
if (card->testAttribute(Qt::WA_TransparentForMouseEvents)) {
|
|
card->setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
|
}
|
|
|
|
bool isDepthCard = (distance > 1.5);
|
|
if (card->property("isDepthCard").toBool() != isDepthCard) {
|
|
card->setProperty("isDepthCard", isDepthCard);
|
|
}
|
|
|
|
|
|
if (isDepthCard) {
|
|
QWidget* actionContainer = nullptr;
|
|
if (card->property("cachedActionPtr").isValid()) {
|
|
actionContainer = reinterpret_cast<QWidget*>(card->property("cachedActionPtr").toULongLong());
|
|
} else {
|
|
for (auto* btn : card->findChildren<QPushButton*>()) {
|
|
if (btn->objectName() == "action-edit-btn" && btn->parentWidget()) {
|
|
actionContainer = btn->parentWidget();
|
|
card->setProperty("cachedActionPtr", reinterpret_cast<qulonglong>(actionContainer));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (actionContainer && actionContainer->isVisible()) {
|
|
actionContainer->hide();
|
|
}
|
|
}
|
|
|
|
qreal angle = logicalIdx * (M_PI / 6.0);
|
|
qreal R = vh / 2.0;
|
|
qreal scale = qMax(0.5, 1.0 - distance * 0.2);
|
|
|
|
int projectedW = vw * scale;
|
|
int projectedH = baseItemHeight * scale;
|
|
int projectedX = (vw - projectedW) / 2;
|
|
int projectedY = centerY + R * qSin(angle) - projectedH / 2.0;
|
|
|
|
|
|
QRect newGeometry(projectedX, projectedY, projectedW, projectedH);
|
|
if (card->geometry() != newGeometry) {
|
|
card->setGeometry(newGeometry);
|
|
}
|
|
|
|
for (auto* child : card->findChildren<QWidget*>()) {
|
|
|
|
if (qobject_cast<QLabel*>(child) || qobject_cast<QPushButton*>(child)) {
|
|
if (!child->property("originalFontSize").isValid()) {
|
|
QFont f = child->font();
|
|
qreal size = f.pixelSize() > 0 ? f.pixelSize() : f.pointSizeF();
|
|
child->setProperty("originalFontSize", size);
|
|
child->setProperty("usePixel", f.pixelSize() > 0);
|
|
}
|
|
|
|
qreal origSize = child->property("originalFontSize").toReal();
|
|
bool usePixel = child->property("usePixel").toBool();
|
|
int newSizeInt = qMax(1, qRound(origSize * scale));
|
|
int lastSizeInt = child->property("lastAppliedFontSize").toInt();
|
|
|
|
if (newSizeInt != lastSizeInt) {
|
|
child->setProperty("lastAppliedFontSize", newSizeInt);
|
|
|
|
QFont newFont = child->font();
|
|
if (usePixel) {
|
|
newFont.setPixelSize(newSizeInt);
|
|
} else {
|
|
newFont.setPointSizeF(newSizeInt);
|
|
}
|
|
child->setFont(newFont);
|
|
}
|
|
}
|
|
|
|
if (QLabel* label = qobject_cast<QLabel*>(child)) {
|
|
if (!label->property("originalIconSize").isValid() &&
|
|
label->minimumWidth() == label->maximumWidth() &&
|
|
label->minimumHeight() == label->maximumHeight() &&
|
|
label->minimumWidth() > 0) {
|
|
label->setProperty("originalIconSize", label->minimumWidth());
|
|
}
|
|
|
|
if (label->property("originalIconSize").isValid()) {
|
|
int origIconSize = label->property("originalIconSize").toInt();
|
|
int newSize = qMax(1, qRound(origIconSize * scale));
|
|
int lastIconSize = label->property("lastAppliedIconSize").toInt();
|
|
|
|
if (newSize != lastIconSize) {
|
|
label->setProperty("lastAppliedIconSize", newSize);
|
|
label->setFixedSize(newSize, newSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (auto* effect = qobject_cast<QGraphicsOpacityEffect*>(card->graphicsEffect())) {
|
|
|
|
if (!qFuzzyCompare(effect->opacity(), opacity)) {
|
|
effect->setOpacity(opacity);
|
|
}
|
|
bool enableEffect = opacity < 0.99;
|
|
if (effect->isEnabled() != enableEffect) {
|
|
effect->setEnabled(enableEffect);
|
|
}
|
|
}
|
|
|
|
zOrderMap.insert(-distance, card);
|
|
}
|
|
|
|
for (auto* card : zOrderMap.values()) {
|
|
card->raise();
|
|
}
|
|
}
|