Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 96 additions & 4 deletions components/images/CachingImage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -24,5 +115,6 @@ Image {

item: root
cacheDir: Qt.resolvedUrl(Paths.imagecache)
preferAnimated: root.preferAnimated
}
}
5 changes: 5 additions & 0 deletions modules/background/Background.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -33,6 +37,7 @@ Loader {

Wallpaper {
id: wallpaper
sessionLock: backgroundLoader.lock ? backgroundLoader.lock.lock : null
}

Visualiser {
Expand Down
41 changes: 35 additions & 6 deletions modules/background/Wallpaper.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions modules/launcher/items/WallpaperItem.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +70,8 @@ Item {
CachingImage {
path: root.modelData.path
smooth: !root.PathView.view.moving
preferAnimated: root.animatePreview
playbackEnabled: root.animatePreview

anchors.fill: parent
}
Expand Down
62 changes: 60 additions & 2 deletions plugin/src/Caelestia/Internal/cachingimagemanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
17 changes: 16 additions & 1 deletion plugin/src/Caelestia/Internal/cachingimagemanager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand All @@ -42,6 +50,8 @@ class CachingImageManager : public QObject {
void pathChanged();
void cachePathChanged();
void usingCacheChanged();
void animatedChanged();
void preferAnimatedChanged();

private:
QString m_shaPath;
Expand All @@ -52,13 +62,18 @@ 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;

[[nodiscard]] qreal effectiveScale() const;
[[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);
};

Expand Down
8 changes: 5 additions & 3 deletions shell.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import "modules/lock"
import Quickshell

ShellRoot {
Background {}
Drawers {}
AreaPicker {}
Lock {
id: lock
}
Background {
lock: lock
}
Drawers {}
AreaPicker {}

Shortcuts {}
BatteryMonitor {}
Expand Down
4 changes: 2 additions & 2 deletions utils/Images.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ pragma Singleton
import Quickshell

Singleton {
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"]
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"]
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg", "gif"]
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg", "gif"]

function isValidImageByName(name: string): bool {
return validImageExtensions.some(t => name.endsWith(`.${t}`));
Expand Down