diff --git a/packaging/arch/README.md b/packaging/arch/README.md index e1b04e1..19cf458 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -31,6 +31,12 @@ sudo pacman -U qemby-*.pkg.tar.zst qemby ``` +If native Wayland causes windowing or OpenGL issues, run with the XCB fallback: + +```fish +QEMBY_FORCE_XCB=1 qemby +``` + ## Uninstall ```fish diff --git a/src/qEmbyApp/components/slidingstackedwidget.cpp b/src/qEmbyApp/components/slidingstackedwidget.cpp index 8cc0a98..e8e536e 100644 --- a/src/qEmbyApp/components/slidingstackedwidget.cpp +++ b/src/qEmbyApp/components/slidingstackedwidget.cpp @@ -1,8 +1,19 @@ #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), @@ -11,8 +22,9 @@ SlidingStackedWidget::SlidingStackedWidget(QWidget *parent) m_isAnimating(false), m_nextIndex(0) { - m_animGroup = new QParallelAnimationGroup(this); - connect(m_animGroup, &QParallelAnimationGroup::finished, this, &SlidingStackedWidget::animationDoneSlot); + m_slideTimer.setTimerType(Qt::PreciseTimer); + connect(&m_slideTimer, &QTimer::timeout, this, + &SlidingStackedWidget::updateSlideAnimation); } void SlidingStackedWidget::setSpeed(int speed) @@ -45,22 +57,7 @@ void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction) if (m_isAnimating) { - - - - bool allTargetsAlive = true; - for (int i = 0; i < m_animGroup->animationCount(); ++i) { - auto *anim = qobject_cast(m_animGroup->animationAt(i)); - if (anim && !anim->targetObject()) { - allTargetsAlive = false; - break; - } - } - - if (allTargetsAlive) { - m_animGroup->stop(); - } - + m_slideTimer.stop(); animationDoneSlot(); } @@ -126,45 +123,50 @@ void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction) QPoint pNext = widgetNext->pos(); QPoint pNow = widgetNow->pos(); - - const bool useSnapshot = - ConfigStore::instance()->get(ConfigKeys::SnapshotNavigation, false); + ConfigStore::instance()->get(ConfigKeys::SnapshotNavigation, true); + + QWidget *animTargetNow = widgetNow; + QWidget *animTargetNext = widgetNext; + bool usingSnapshotAnimation = false; if (useSnapshot) { - QPixmap snapshot = widgetNow->grab(); - m_snapshotLabel = new QLabel(this); - m_snapshotLabel->setPixmap(snapshot); - m_snapshotLabel->setGeometry(widgetNow->geometry()); - m_snapshotLabel->show(); - widgetNow->hide(); + 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; } - - widgetNext->move(pNext.x() + offsetX, pNext.y() + offsetY); - widgetNext->show(); - widgetNext->raise(); + if (usingSnapshotAnimation) { + animTargetNow = m_currentSnapshotLabel; + animTargetNext = m_nextSnapshotLabel; + widgetNow->hide(); + widgetNext->hide(); + } else { + clearSnapshotLabels(); - - QObject *animTarget = useSnapshot ? static_cast(m_snapshotLabel) - : static_cast(widgetNow); - QPropertyAnimation *animNow = new QPropertyAnimation(animTarget, "pos"); - animNow->setDuration(m_speed); - animNow->setEasingCurve(m_animationType); - animNow->setStartValue(pNow); - animNow->setEndValue(QPoint(pNow.x() - offsetX, pNow.y() - offsetY)); + widgetNext->move(pNext.x() + offsetX, pNext.y() + offsetY); + widgetNext->show(); + widgetNext->raise(); + } - - QPropertyAnimation *animNext = new QPropertyAnimation(widgetNext, "pos"); - animNext->setDuration(m_speed); - animNext->setEasingCurve(m_animationType); - animNext->setStartValue(widgetNext->pos()); - animNext->setEndValue(pNext); - - m_animGroup->clear(); - m_animGroup->addAnimation(animNow); - m_animGroup->addAnimation(animNext); - m_animGroup->start(); + startSlideAnimation(animTargetNow, + pNow, + QPoint(pNow.x() - offsetX, pNow.y() - offsetY), + animTargetNext, + QPoint(pNext.x() + offsetX, pNext.y() + offsetY), + pNext); } void SlidingStackedWidget::disposeWidgetWhenSafe(QWidget *widget) @@ -186,14 +188,124 @@ void SlidingStackedWidget::disposeWidgetWhenSafe(QWidget *widget) } } +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() { - - if (m_snapshotLabel) { - m_snapshotLabel->hide(); - m_snapshotLabel->deleteLater(); - m_snapshotLabel = nullptr; - } + m_slideTimer.stop(); + m_animWidgetNow.clear(); + m_animWidgetNext.clear(); + clearSnapshotLabels(); if (m_nextIndex >= 0 && m_nextIndex < count()) { diff --git a/src/qEmbyApp/components/slidingstackedwidget.h b/src/qEmbyApp/components/slidingstackedwidget.h index bdc9a48..5e25ef2 100644 --- a/src/qEmbyApp/components/slidingstackedwidget.h +++ b/src/qEmbyApp/components/slidingstackedwidget.h @@ -2,11 +2,13 @@ #define SLIDINGSTACKEDWIDGET_H #include -#include -#include #include +#include #include +#include #include +#include +#include class QLabel; @@ -42,15 +44,31 @@ private Q_SLOTS: void animationDoneSlot(); private: + QLabel *createSnapshotLabel(QWidget *source, const QRect &geometry); + void clearSnapshotLabels(); + void startSlideAnimation(QWidget *widgetNow, const QPoint &nowStart, + const QPoint &nowEnd, QWidget *widgetNext, + const QPoint &nextStart, + const QPoint &nextEnd); + void updateSlideAnimation(); + int animationFrameIntervalMs() const; void flushPendingWidgetDisposals(); int m_speed; QEasingCurve::Type m_animationType; - QParallelAnimationGroup *m_animGroup; + QTimer m_slideTimer; + QElapsedTimer m_slideClock; + QPointer m_animWidgetNow; + QPointer m_animWidgetNext; + QPoint m_animNowStart; + QPoint m_animNowEnd; + QPoint m_animNextStart; + QPoint m_animNextEnd; bool m_isAnimating; int m_nextIndex; QList> m_pendingWidgetDisposals; - QLabel* m_snapshotLabel = nullptr; + QLabel* m_currentSnapshotLabel = nullptr; + QLabel* m_nextSnapshotLabel = nullptr; }; #endif diff --git a/src/qEmbyApp/main.cpp b/src/qEmbyApp/main.cpp index 69e38c0..f788480 100644 --- a/src/qEmbyApp/main.cpp +++ b/src/qEmbyApp/main.cpp @@ -27,17 +27,19 @@ int main(int argc, char *argv[]) { #if defined(Q_OS_LINUX) const QByteArray qpaPlatform = qgetenv("QT_QPA_PLATFORM"); - const QByteArray sessionType = qgetenv("XDG_SESSION_TYPE").toLower(); - if (qpaPlatform.isEmpty() && sessionType == "wayland" && - !qgetenv("DISPLAY").isEmpty()) { + const QByteArray forceXcb = qgetenv("QEMBY_FORCE_XCB").toLower(); + if (qpaPlatform.isEmpty() && + (forceXcb == "1" || forceXcb == "true" || forceXcb == "yes")) { qputenv("QT_QPA_PLATFORM", "xcb"); } #endif QSurfaceFormat format; +#if !defined(Q_OS_LINUX) format.setVersion(3, 3); format.setProfile(QSurfaceFormat::CoreProfile); +#endif format.setDepthBufferSize(24); format.setStencilBufferSize(8); format.setSwapInterval(1); diff --git a/src/qEmbyApp/mainwindow.cpp b/src/qEmbyApp/mainwindow.cpp index a1b386e..c0bc68e 100644 --- a/src/qEmbyApp/mainwindow.cpp +++ b/src/qEmbyApp/mainwindow.cpp @@ -48,6 +48,36 @@ #include #include +namespace { +bool canAnimateTopLevelWindowPosition() +{ +#if defined(Q_OS_LINUX) + return !QGuiApplication::platformName().contains(QStringLiteral("wayland"), + Qt::CaseInsensitive); +#else + return true; +#endif +} + +QRect centeredRectWithin(const QRect &sourceGeometry, const QSize &targetSize, + const QRect &bounds) +{ + QRect target(QPoint(0, 0), targetSize); + target.moveCenter(sourceGeometry.center()); + + if (target.left() < bounds.left()) + target.moveLeft(bounds.left()); + if (target.top() < bounds.top()) + target.moveTop(bounds.top()); + if (target.right() > bounds.right()) + target.moveRight(bounds.right()); + if (target.bottom() > bounds.bottom()) + target.moveBottom(bounds.bottom()); + + return target; +} +} + static inline void emulateLeaveEvent(QWidget *widget) { Q_ASSERT(widget); if (!widget) return; @@ -966,6 +996,51 @@ void MainWindow::navigateToHome() return; } + if (!canAnimateTopLevelWindowPosition()) { + hide(); + setWindowOpacity(0.0); + setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); + setMinimumSize(0, 0); + + const QString windowState = ConfigStore::instance()->get( + ConfigKeys::StartupWindowState, "normal"); + const bool shouldMaximize = (windowState == "maximized"); + + m_loginView->setGraphicsEffect(nullptr); + m_homeView->setGraphicsEffect(nullptr); + m_viewStack->setCurrentWidget(m_homeView); + +#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC) + if (auto* minBtn = findChild("min-button")) minBtn->show(); + if (auto* maxBtn = findChild("max-button")) maxBtn->show(); +#endif + + if (shouldMaximize) { + showMaximized(); + } else { + resize(1280, 800); + } + setMinimumSize(900, 600); + show(); + raise(); + activateWindow(); + + auto *fadeIn = new QPropertyAnimation(this, "windowOpacity", this); + fadeIn->setDuration(180); + fadeIn->setStartValue(0.0); + fadeIn->setEndValue(1.0); + fadeIn->setEasingCurve(QEasingCurve::OutCubic); + + connect(fadeIn, &QPropertyAnimation::finished, this, [this]() { + setWindowOpacity(1.0); + m_viewTransitionInProgress = false; + }); + QTimer::singleShot(0, fadeIn, [fadeIn]() { + fadeIn->start(QAbstractAnimation::DeleteWhenStopped); + }); + return; + } + auto *loginOpacity = new QGraphicsOpacityEffect(m_loginView); loginOpacity->setOpacity(1.0); @@ -979,6 +1054,7 @@ void MainWindow::navigateToHome() connect(fadeOut, &QPropertyAnimation::finished, this, [this]() { setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); + setMinimumSize(0, 0); QString windowState = ConfigStore::instance()->get( @@ -994,32 +1070,13 @@ void MainWindow::navigateToHome() int targetWidth = 1280; int targetHeight = 800; - targetGeo = QRect( - startGeo.x() - (targetWidth - startGeo.width()) / 2, - startGeo.y() - (targetHeight - startGeo.height()) / 2, - targetWidth, targetHeight); - - - QRect screenGeo = screen()->availableGeometry(); - if (targetGeo.left() < screenGeo.left()) - targetGeo.moveLeft(screenGeo.left()); - if (targetGeo.top() < screenGeo.top()) - targetGeo.moveTop(screenGeo.top()); - if (targetGeo.right() > screenGeo.right()) - targetGeo.moveRight(screenGeo.right()); - if (targetGeo.bottom() > screenGeo.bottom()) - targetGeo.moveBottom(screenGeo.bottom()); + targetGeo = centeredRectWithin(startGeo, + QSize(targetWidth, targetHeight), + screenGeo); } - auto *geoAnim = new QPropertyAnimation(this, "geometry", this); - - geoAnim->setDuration(shouldMaximize ? 500 : 350); - geoAnim->setStartValue(startGeo); - geoAnim->setEndValue(targetGeo); - geoAnim->setEasingCurve(QEasingCurve::InOutCubic); - - connect(geoAnim, &QPropertyAnimation::finished, this, [this, shouldMaximize]() { + auto finishWindowTransition = [this, shouldMaximize]() { setMinimumSize(900, 600); @@ -1055,7 +1112,16 @@ void MainWindow::navigateToHome() m_viewTransitionInProgress = false; }); fadeIn->start(QAbstractAnimation::DeleteWhenStopped); - }); + }; + + auto *geoAnim = new QPropertyAnimation(this, "geometry", this); + + geoAnim->setDuration(shouldMaximize ? 500 : 350); + geoAnim->setStartValue(startGeo); + geoAnim->setEndValue(targetGeo); + geoAnim->setEasingCurve(QEasingCurve::InOutCubic); + connect(geoAnim, &QPropertyAnimation::finished, this, + finishWindowTransition); geoAnim->start(QAbstractAnimation::DeleteWhenStopped); }); fadeOut->start(QAbstractAnimation::DeleteWhenStopped); @@ -1132,20 +1198,11 @@ void MainWindow::navigateToLogin() { QRect startGeo = geometry(); int targetWidth = m_defaultWidth; int targetHeight = m_defaultHeight; - - - QRect targetGeo(startGeo.x() + (startGeo.width() - targetWidth) / 2, - startGeo.y() + (startGeo.height() - targetHeight) / 2, - targetWidth, targetHeight); + QRect targetGeo = centeredRectWithin( + startGeo, QSize(targetWidth, targetHeight), + screen()->availableGeometry()); - auto *geoAnim = new QPropertyAnimation(this, "geometry", this); - - geoAnim->setDuration(startGeo.width() > 1300 ? 500 : 350); - geoAnim->setStartValue(startGeo); - geoAnim->setEndValue(targetGeo); - geoAnim->setEasingCurve(QEasingCurve::InOutCubic); - - connect(geoAnim, &QPropertyAnimation::finished, this, [this]() { + auto finishWindowTransition = [this]() { @@ -1172,8 +1229,22 @@ void MainWindow::navigateToLogin() { m_viewTransitionInProgress = false; }); fadeIn->start(QAbstractAnimation::DeleteWhenStopped); - }); - geoAnim->start(QAbstractAnimation::DeleteWhenStopped); + }; + + if (canAnimateTopLevelWindowPosition()) { + auto *geoAnim = new QPropertyAnimation(this, "geometry", this); + + geoAnim->setDuration(startGeo.width() > 1300 ? 500 : 350); + geoAnim->setStartValue(startGeo); + geoAnim->setEndValue(targetGeo); + geoAnim->setEasingCurve(QEasingCurve::InOutCubic); + connect(geoAnim, &QPropertyAnimation::finished, this, + finishWindowTransition); + geoAnim->start(QAbstractAnimation::DeleteWhenStopped); + } else { + resize(targetGeo.size()); + finishWindowTransition(); + } }); fadeOut->start(QAbstractAnimation::DeleteWhenStopped); } diff --git a/src/qEmbyApp/utils/smoothscrollcontroller.cpp b/src/qEmbyApp/utils/smoothscrollcontroller.cpp index ac75b60..324acb7 100644 --- a/src/qEmbyApp/utils/smoothscrollcontroller.cpp +++ b/src/qEmbyApp/utils/smoothscrollcontroller.cpp @@ -1,6 +1,7 @@ #include "smoothscrollcontroller.h" #include +#include #include #include @@ -150,6 +151,27 @@ int SmoothScrollController::wheelDelta(const QWheelEvent *event, return orientation == Qt::Horizontal ? angleDelta.x() : angleDelta.y(); } +int SmoothScrollController::frameIntervalMs() const +{ + if (!m_scrollBar) { + return 16; + } + + const QScreen *targetScreen = m_scrollBar->screen(); + if (!targetScreen && m_scrollBar->window()) { + targetScreen = m_scrollBar->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 SmoothScrollController::startTicker() { if (!m_scrollBar || m_scrollBar->value() == m_targetValue) { @@ -157,6 +179,7 @@ void SmoothScrollController::startTicker() return; } + m_ticker.setInterval(frameIntervalMs()); if (!m_ticker.isActive()) { m_frameClock.restart(); m_ticker.start(); diff --git a/src/qEmbyApp/utils/smoothscrollcontroller.h b/src/qEmbyApp/utils/smoothscrollcontroller.h index f4f4f6e..d9e9aa3 100644 --- a/src/qEmbyApp/utils/smoothscrollcontroller.h +++ b/src/qEmbyApp/utils/smoothscrollcontroller.h @@ -29,6 +29,7 @@ private: int boundedValue(int value) const; int wheelDelta(const QWheelEvent *event, Qt::Orientation orientation) const; + int frameIntervalMs() const; void startTicker(); QScrollBar *m_scrollBar = nullptr; diff --git a/src/qEmbyCore/config/configstore.cpp b/src/qEmbyCore/config/configstore.cpp index 2663aaf..f3dc00b 100644 --- a/src/qEmbyCore/config/configstore.cpp +++ b/src/qEmbyCore/config/configstore.cpp @@ -66,6 +66,9 @@ ConfigStore::ConfigStore(QObject* parent) : QObject(parent) { if (!m_settings->contains(ConfigKeys::ImageCacheDuration)) { m_settings->setValue(ConfigKeys::ImageCacheDuration, "7"); } + if (!m_settings->contains(ConfigKeys::SnapshotNavigation)) { + m_settings->setValue(ConfigKeys::SnapshotNavigation, true); + } } ConfigStore::~ConfigStore() {