#include "slidingstackedwidget.h" #include #include #include #include "../../qEmbyCore/config/configstore.h" #include "config/config_keys.h" #include namespace { QPoint interpolatePoint(const QPoint &start, const QPoint &end, qreal progress) { return QPoint( qRound(start.x() + (end.x() - start.x()) * progress), qRound(start.y() + (end.y() - start.y()) * progress)); } } SlidingStackedWidget::SlidingStackedWidget(QWidget *parent) : QStackedWidget(parent), m_speed(350), m_animationType(QEasingCurve::OutCubic), m_isAnimating(false), m_nextIndex(0) { m_slideTimer.setTimerType(Qt::PreciseTimer); connect(&m_slideTimer, &QTimer::timeout, this, &SlidingStackedWidget::updateSlideAnimation); } void SlidingStackedWidget::setSpeed(int speed) { m_speed = speed; } void SlidingStackedWidget::setEasingCurve(QEasingCurve::Type curveType) { m_animationType = curveType; } void SlidingStackedWidget::slideInWgt(QWidget *widget, SlideDirection direction) { int idx = indexOf(widget); if (idx != -1) { slideInIdx(idx, direction); } } void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction) { if (idx < 0 || idx >= count()) return; if (currentIndex() == idx && !m_isAnimating) return; if (m_isAnimating) { m_slideTimer.stop(); animationDoneSlot(); } bool reduceAnimations = ConfigStore::instance()->get(ConfigKeys::UiAnimations, false); if (reduceAnimations) { m_nextIndex = idx; setCurrentIndex(idx); QWidget *w = widget(idx); if (w) w->raise(); flushPendingWidgetDisposals(); Q_EMIT animationFinished(); return; } m_isAnimating = true; m_nextIndex = idx; int now = currentIndex(); QWidget *widgetNow = widget(now); QWidget *widgetNext = widget(idx); if (!widgetNow || !widgetNext) { m_isAnimating = false; if (widgetNext) { setCurrentIndex(idx); } return; } int width = this->width(); int height = this->height(); int offsetX = 0; int offsetY = 0; SlideDirection actualDirection = direction; if (direction == Automatic) { if (now < idx) actualDirection = RightToLeft; else actualDirection = LeftToRight; } if (actualDirection == RightToLeft) { offsetX = width; offsetY = 0; } else if (actualDirection == LeftToRight) { offsetX = -width; offsetY = 0; } else if (actualDirection == BottomToTop) { offsetX = 0; offsetY = height; } else if (actualDirection == TopToBottom) { offsetX = 0; offsetY = -height; } widgetNext->setGeometry(0, 0, width, height); QPoint pNext = widgetNext->pos(); QPoint pNow = widgetNow->pos(); const bool useSnapshot = ConfigStore::instance()->get(ConfigKeys::SnapshotNavigation, true); QWidget *animTargetNow = widgetNow; QWidget *animTargetNext = widgetNext; bool usingSnapshotAnimation = false; if (useSnapshot) { clearSnapshotLabels(); m_currentSnapshotLabel = createSnapshotLabel(widgetNow, QRect(pNow, QSize(width, height))); widgetNext->show(); widgetNext->raise(); m_nextSnapshotLabel = createSnapshotLabel(widgetNext, QRect(QPoint(pNext.x() + offsetX, pNext.y() + offsetY), QSize(width, height))); usingSnapshotAnimation = m_currentSnapshotLabel && m_nextSnapshotLabel; } if (usingSnapshotAnimation) { animTargetNow = m_currentSnapshotLabel; animTargetNext = m_nextSnapshotLabel; widgetNow->hide(); widgetNext->hide(); } else { clearSnapshotLabels(); widgetNext->move(pNext.x() + offsetX, pNext.y() + offsetY); widgetNext->show(); widgetNext->raise(); } startSlideAnimation(animTargetNow, pNow, QPoint(pNow.x() - offsetX, pNow.y() - offsetY), animTargetNext, QPoint(pNext.x() + offsetX, pNext.y() + offsetY), pNext); } void SlidingStackedWidget::disposeWidgetWhenSafe(QWidget *widget) { if (!widget) { return; } const QPointer safeWidget(widget); if (!m_pendingWidgetDisposals.contains(safeWidget)) { m_pendingWidgetDisposals.append(safeWidget); } if (!m_isAnimating) { flushPendingWidgetDisposals(); } } QLabel *SlidingStackedWidget::createSnapshotLabel(QWidget *source, const QRect &geometry) { if (!source || geometry.size().isEmpty()) { return nullptr; } QPixmap snapshot = source->grab(QRect(QPoint(0, 0), geometry.size())); if (snapshot.isNull()) { return nullptr; } auto *label = new QLabel(this); label->setAttribute(Qt::WA_TransparentForMouseEvents, true); label->setPixmap(snapshot); label->setGeometry(geometry); label->show(); label->raise(); return label; } void SlidingStackedWidget::clearSnapshotLabels() { if (m_currentSnapshotLabel) { m_currentSnapshotLabel->hide(); m_currentSnapshotLabel->deleteLater(); m_currentSnapshotLabel = nullptr; } if (m_nextSnapshotLabel) { m_nextSnapshotLabel->hide(); m_nextSnapshotLabel->deleteLater(); m_nextSnapshotLabel = nullptr; } } void SlidingStackedWidget::startSlideAnimation(QWidget *widgetNow, const QPoint &nowStart, const QPoint &nowEnd, QWidget *widgetNext, const QPoint &nextStart, const QPoint &nextEnd) { m_animWidgetNow = widgetNow; m_animWidgetNext = widgetNext; m_animNowStart = nowStart; m_animNowEnd = nowEnd; m_animNextStart = nextStart; m_animNextEnd = nextEnd; if (!m_animWidgetNow || !m_animWidgetNext || m_speed <= 0) { animationDoneSlot(); return; } m_slideTimer.setInterval(animationFrameIntervalMs()); m_slideClock.restart(); updateSlideAnimation(); m_slideTimer.start(); } void SlidingStackedWidget::updateSlideAnimation() { if (!m_animWidgetNow || !m_animWidgetNext) { animationDoneSlot(); return; } const qreal elapsedMs = m_slideClock.isValid() ? m_slideClock.nsecsElapsed() / 1000000.0 : static_cast(m_speed); const qreal linearProgress = qBound(0.0, elapsedMs / qMax(1, m_speed), 1.0); const qreal easedProgress = QEasingCurve(m_animationType).valueForProgress(linearProgress); m_animWidgetNow->move( interpolatePoint(m_animNowStart, m_animNowEnd, easedProgress)); m_animWidgetNext->move( interpolatePoint(m_animNextStart, m_animNextEnd, easedProgress)); if (linearProgress >= 1.0) { animationDoneSlot(); } } int SlidingStackedWidget::animationFrameIntervalMs() const { const QScreen *targetScreen = screen(); if (!targetScreen && window()) { targetScreen = window()->screen(); } const qreal refreshRate = targetScreen ? targetScreen->refreshRate() : 0.0; if (refreshRate <= 0.0) { return 16; } return qBound(4, static_cast(std::lround(1000.0 / refreshRate)), 16); } void SlidingStackedWidget::animationDoneSlot() { m_slideTimer.stop(); m_animWidgetNow.clear(); m_animWidgetNext.clear(); clearSnapshotLabels(); if (m_nextIndex >= 0 && m_nextIndex < count()) { setCurrentIndex(m_nextIndex); QWidget *w = widget(m_nextIndex); if (w) { w->raise(); } } m_isAnimating = false; flushPendingWidgetDisposals(); Q_EMIT animationFinished(); } void SlidingStackedWidget::flushPendingWidgetDisposals() { if (m_isAnimating || m_pendingWidgetDisposals.isEmpty()) { return; } const auto widgetsToDispose = m_pendingWidgetDisposals; m_pendingWidgetDisposals.clear(); for (const QPointer &widget : widgetsToDispose) { if (!widget) { continue; } if (indexOf(widget) != -1) { removeWidget(widget); } widget->deleteLater(); } }