diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index e8f957a7d..8526c16e5 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -3,19 +3,110 @@ import Caelestia.Internal import Quickshell import QtQuick -Image { +Item { id: root property alias path: manager.path - asynchronous: true - fillMode: Image.PreserveAspectCrop + property url source: "" + property size sourceSize: Qt.size(0, 0) + + property int fillMode: Image.PreserveAspectCrop + property bool smooth: true + property bool asynchronous: true + + property bool playbackEnabled: true + property bool pauseWhenHidden: true + property bool preferAnimated: true + + readonly property bool animated: manager.animated + readonly property Item contentItem: loader.status === Loader.Ready ? loader.item : null + readonly property int status: contentItem ? contentItem.status : Image.Null + + implicitWidth: 0 + implicitHeight: 0 + + function restart(): void { + if (!animated || !contentItem || !("currentFrame" in contentItem)) + return; + + contentItem.currentFrame = 0; + } + + function updateAnimatedPause(): void { + if (!animated || !contentItem || !("paused" in contentItem)) + return; + + const shouldPause = !playbackEnabled || (pauseWhenHidden && !visible); + if (contentItem.paused !== shouldPause) + contentItem.paused = shouldPause; + } + + onPlaybackEnabledChanged: updateAnimatedPause() + onPauseWhenHiddenChanged: updateAnimatedPause() + onVisibleChanged: updateAnimatedPause() + onAnimatedChanged: updateAnimatedPause() Connections { target: QsWindow.window function onDevicePixelRatioChanged(): void { - manager.updateSource(); + if (!manager.animated || !root.preferAnimated) + manager.updateSource(); + } + } + + Image { + id: animatedPlaceholder + + anchors.fill: parent + asynchronous: root.asynchronous + cache: false + fillMode: root.fillMode + smooth: root.smooth + visible: manager.animated && root.preferAnimated && root.source && (!root.contentItem || root.contentItem.status !== Image.Ready) + source: root.source + sourceSize: root.sourceSize + } + + Loader { + id: loader + + anchors.fill: parent + active: !!root.path + asynchronous: true + + sourceComponent: manager.animated && root.preferAnimated ? animatedComponent : staticComponent + + onStatusChanged: { + if (status === Loader.Ready) + root.updateAnimatedPause(); + } + } + + Component { + id: staticComponent + + Image { + anchors.fill: parent + asynchronous: root.asynchronous + fillMode: root.fillMode + smooth: root.smooth + source: root.source + sourceSize: root.sourceSize + } + } + + Component { + id: animatedComponent + + AnimatedImage { + anchors.fill: parent + cache: false + fillMode: root.fillMode + smooth: root.smooth + source: root.source + sourceSize: root.sourceSize } } @@ -24,5 +115,6 @@ Image { item: root cacheDir: Qt.resolvedUrl(Paths.imagecache) + preferAnimated: root.preferAnimated } } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index fbacfabb8..9ba2e7369 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -9,9 +9,13 @@ import Quickshell.Wayland import QtQuick Loader { + id: backgroundLoader + asynchronous: true active: Config.background.enabled + property var lock + sourceComponent: Variants { model: Quickshell.screens @@ -33,6 +37,7 @@ Loader { Wallpaper { id: wallpaper + sessionLock: backgroundLoader.lock ? backgroundLoader.lock.lock : null } Visualiser { diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 233dacb72..ae87401c0 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -12,7 +12,10 @@ Item { id: root property string source: Wallpapers.current - property Image current: one + property CachingImage current: one + readonly property Item imageItem: (current && current.contentItem) ? current.contentItem : null + property var sessionLock: null + readonly property bool sessionLocked: sessionLock ? sessionLock.secure : false anchors.fill: parent @@ -112,20 +115,46 @@ Item { id: img function update(): void { - if (path === root.source) + if (!root.source) return; + + if (path === root.source) { root.current = this; - else - path = root.source; + return; + } + + const target = root.source; + path = target; + + if (img.animated) { + Qt.callLater(() => { + if (img.path === target && root.source === target) + root.current = img; + }); + } } anchors.fill: parent opacity: 0 scale: Wallpapers.showPreview ? 1 : 0.8 + playbackEnabled: root.current === img && !root.sessionLocked onStatusChanged: { - if (status === Image.Ready) - root.current = this; + if (status === Image.Ready) root.current = this; + } + + onPlaybackEnabledChanged: { + if (root.current === img && img.animated && playbackEnabled) + img.restart(); + } + + Connections { + target: root + + function onCurrentChanged(): void { + if (root.current === img && img.animated) + img.restart(); + } } states: State { diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 9fdac3f38..da9c16601 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -13,6 +13,9 @@ Item { required property FileSystemEntry modelData required property PersistentProperties visibilities + // Play the animated wallpaper preview? + property bool animatePreview: false + scale: 0.5 opacity: 0 z: PathView.z ?? 0 @@ -67,8 +70,9 @@ Item { CachingImage { path: root.modelData.path smooth: !root.PathView.view.moving + preferAnimated: root.animatePreview + playbackEnabled: root.animatePreview cache: true - anchors.fill: parent } } diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index 1c15cd203..6c6ddaf72 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -88,18 +88,68 @@ void CachingImageManager::setPath(const QString& path) { m_path = path; emit pathChanged(); + // eww but I'll do it again here + const bool animated = !path.isEmpty() && isAnimated(path); + if (m_animated != animated) { + m_animated = animated; + emit animatedChanged(); + } + if (!path.isEmpty()) { updateSource(path); } } +void CachingImageManager::setPreferAnimated(bool preferAnimated) { + if (m_preferAnimated == preferAnimated) { + return; + } + + m_preferAnimated = preferAnimated; + emit preferAnimatedChanged(); + + if (!m_path.isEmpty()) { + updateSource(m_path); + } +} + void CachingImageManager::updateSource() { updateSource(m_path); } void CachingImageManager::updateSource(const QString& path) { - if (path.isEmpty() || path == m_shaPath) { - // Path is empty or already calculating sha for path + if (path.isEmpty()) { + return; + } + + const bool animated = isAnimated(path); + if (m_animated != animated) { + m_animated = animated; + emit animatedChanged(); + } + + const bool useAnimation = animated && m_preferAnimated; + + if (useAnimation) { + const QSize size = effectiveSize(); + + if (!m_item || !size.width() || !size.height()) { + m_shaPath.clear(); + return; + } + + const QUrl cache; + if (m_cachePath != cache) { + m_cachePath = cache; + emit cachePathChanged(); + } + + m_item->setProperty("source", QUrl::fromLocalFile(path)); + m_shaPath.clear(); + return; + } + + if (path == m_shaPath) { return; } @@ -220,4 +270,12 @@ QString CachingImageManager::sha256sum(const QString& path) { return hash.result().toHex(); } +bool CachingImageManager::isAnimated(const QString& path) { + QImageReader reader(path); + if (!reader.canRead() || !reader.supportsAnimation()) { + return false; + } + return reader.imageCount() > 1; +} + } // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp index 3611699b6..5f0882ce7 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -16,10 +16,14 @@ class CachingImageManager : public QObject { Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) + Q_PROPERTY(bool animated READ animated NOTIFY animatedChanged) + Q_PROPERTY(bool preferAnimated READ preferAnimated WRITE setPreferAnimated NOTIFY preferAnimatedChanged) + public: explicit CachingImageManager(QObject* parent = nullptr) : QObject(parent) - , m_item(nullptr) {} + , m_item(nullptr) + , m_animated(false) {} [[nodiscard]] QQuickItem* item() const; void setItem(QQuickItem* item); @@ -32,6 +36,10 @@ class CachingImageManager : public QObject { [[nodiscard]] QUrl cachePath() const; + [[nodiscard]] bool animated() const { return m_animated; } + [[nodiscard]] bool preferAnimated() const { return m_preferAnimated; } + void setPreferAnimated(bool preferAnimated); + Q_INVOKABLE void updateSource(); Q_INVOKABLE void updateSource(const QString& path); @@ -42,6 +50,8 @@ class CachingImageManager : public QObject { void pathChanged(); void cachePathChanged(); void usingCacheChanged(); + void animatedChanged(); + void preferAnimatedChanged(); private: QString m_shaPath; @@ -52,6 +62,9 @@ class CachingImageManager : public QObject { QString m_path; QUrl m_cachePath; + bool m_animated; + bool m_preferAnimated = true; + QMetaObject::Connection m_widthConn; QMetaObject::Connection m_heightConn; @@ -59,6 +72,8 @@ class CachingImageManager : public QObject { [[nodiscard]] QSize effectiveSize() const; void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; + + [[nodiscard]] static bool isAnimated(const QString& path); [[nodiscard]] static QString sha256sum(const QString& path); }; diff --git a/shell.qml b/shell.qml index 3ce777699..160272c81 100644 --- a/shell.qml +++ b/shell.qml @@ -10,12 +10,14 @@ import "modules/lock" import Quickshell ShellRoot { - Background {} - Drawers {} - AreaPicker {} Lock { id: lock } + Background { + lock: lock + } + Drawers {} + AreaPicker {} Shortcuts {} BatteryMonitor {} diff --git a/utils/Images.qml b/utils/Images.qml index ac76f5118..6cfbcd06f 100644 --- a/utils/Images.qml +++ b/utils/Images.qml @@ -3,8 +3,8 @@ pragma Singleton import Quickshell Singleton { - readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] - readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] + readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg", "gif"] + readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg", "gif"] function isValidImageByName(name: string): bool { return validImageExtensions.some(t => name.endsWith(`.${t}`));