diff options
| -rw-r--r-- | components/filedialog/FolderContents.qml | 1 | ||||
| -rw-r--r-- | components/images/CachingIconImage.qml | 45 | ||||
| -rw-r--r-- | components/images/CachingImage.qml | 37 | ||||
| -rw-r--r-- | plugin/src/Caelestia/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | plugin/src/Caelestia/cachingimagemanager.cpp | 122 | ||||
| -rw-r--r-- | plugin/src/Caelestia/cachingimagemanager.hpp | 57 | ||||
| -rw-r--r-- | plugin/src/Caelestia/cutils.hpp | 3 |
7 files changed, 221 insertions, 45 deletions
diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 1f67cd3..afd2ed1 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -148,7 +148,6 @@ Item { anchors.top: parent.top anchors.topMargin: Appearance.padding.normal - asynchronous: true implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 source: { if (!item.fileIsDir) diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml index 522a947..715d379 100644 --- a/components/images/CachingIconImage.qml +++ b/components/images/CachingIconImage.qml @@ -1,31 +1,42 @@ +pragma ComponentBehavior: Bound + +import qs.utils +import Quickshell.Widgets import QtQuick Item { - property alias asynchronous: image.asynchronous - property alias status: image.status - property alias mipmap: image.mipmap - property alias backer: image + id: root - property real implicitSize + readonly property int status: loader.item?.status ?? Image.Null readonly property real actualSize: Math.min(width, height) - + property real implicitSize property url source - onSourceChanged: { - if (source?.toString().startsWith("image://icon/")) - // Directly skip the path prop and treat like a normal Image component - image.source = source; - else if (source) - image.path = source; - } - implicitWidth: implicitSize implicitHeight: implicitSize - CachingImage { - id: image + Loader { + id: loader anchors.fill: parent - fillMode: Image.PreserveAspectFit + sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null + } + + Component { + id: cachingImage + + CachingImage { + path: Paths.strip(root.source) + fillMode: Image.PreserveAspectFit + } + } + + Component { + id: iconImage + + IconImage { + source: root.source + asynchronous: true + } } } diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index add459a..07b98b6 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -1,44 +1,29 @@ import qs.utils import Caelestia -import Quickshell -import Quickshell.Io import QtQuick Image { id: root - property string path - property string hash - readonly property url cachePath: `${Paths.imagecache}/${hash}@${effectiveWidth}x${effectiveHeight}.png` + property alias path: manager.path - readonly property real effectiveScale: QsWindow.window?.devicePixelRatio ?? 1 - readonly property int effectiveWidth: Math.ceil(width * effectiveScale) - readonly property int effectiveHeight: Math.ceil(height * effectiveScale) + property int sourceWidth + property int sourceHeight asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: effectiveWidth - sourceSize.height: effectiveHeight - - onPathChanged: shaProc.exec(["sha256sum", Paths.strip(path)]) - - onCachePathChanged: { - if (hash) - source = cachePath; - } + sourceSize.width: sourceWidth + sourceSize.height: sourceHeight onStatusChanged: { - if (source == cachePath && status === Image.Error) - source = path; - else if (source == path && status === Image.Ready) - CUtils.saveItem(this, cachePath); + if (!manager.usingCache && status === Image.Ready) + CUtils.saveItem(this, manager.cachePath); } - Process { - id: shaProc + CachingImageManager { + id: manager - stdout: StdioCollector { - onStreamFinished: root.hash = text.split(" ")[0] - } + item: root + cacheDir: Paths.imagecache } } diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 61d7f7e..3ea0225 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -3,6 +3,7 @@ qt_add_qml_module(caelestia VERSION 0.1 SOURCES cutils.hpp cutils.cpp + cachingimagemanager.hpp cachingimagemanager.cpp ) qt_query_qml_module(caelestia diff --git a/plugin/src/Caelestia/cachingimagemanager.cpp b/plugin/src/Caelestia/cachingimagemanager.cpp new file mode 100644 index 0000000..d557203 --- /dev/null +++ b/plugin/src/Caelestia/cachingimagemanager.cpp @@ -0,0 +1,122 @@ +#include "cachingimagemanager.hpp" + +#include <qobject.h> +#include <QtQuick/QQuickItem> +#include <QtQuick/QQuickWindow> +#include <QCryptographicHash> +#include <QThreadPool> +#include <QFile> + +qreal CachingImageManager::effectiveScale() const { + if (m_item->window() && m_item->window()->screen()) { + return m_item->window()->screen()->devicePixelRatio(); + } + + return 1.0; +} + +int CachingImageManager::effectiveWidth() const { + int width = std::ceil(m_item->width() * effectiveScale()); + m_item->setProperty("sourceWidth", width); + return width; +} + +int CachingImageManager::effectiveHeight() const { + int height = std::ceil(m_item->height() * effectiveScale()); + m_item->setProperty("sourceHeight", height); + return height; +} + +QQuickItem* CachingImageManager::item() const { + return m_item; +} + +void CachingImageManager::setItem(QQuickItem* item) { + if (m_item == item) { + return; + } + + m_item = item; + emit itemChanged(); +} + +QUrl CachingImageManager::cacheDir() const { + return m_cacheDir; +} + +void CachingImageManager::setCacheDir(const QUrl& cacheDir) { + if (m_cacheDir == cacheDir) { + return; + } + + m_cacheDir = cacheDir; + emit cacheDirChanged(); +} + +QString CachingImageManager::path() const { + return m_path; +} + +void CachingImageManager::setPath(const QString& path) { + if (m_path == path) { + return; + } + + m_path = path; + emit pathChanged(); + + if (!path.isEmpty()) { + QThreadPool::globalInstance()->start([path, this] { + const QString filename = QString("%1@%2x%3.png") + .arg(sha256sum(path)) + .arg(effectiveWidth()) + .arg(effectiveHeight()); + + m_cachePath = m_cacheDir.resolved(QUrl(filename)); + emit cachePathChanged(); + + if (!m_cachePath.isLocalFile()) { + qWarning() << "CachingImageManager::setPath: cachePath" << m_cachePath << "is not a local file"; + return; + } + + if (QFile::exists(m_cachePath.toLocalFile())) { + QMetaObject::invokeMethod(m_item, [this]() { + m_item->setProperty("source", m_cachePath); + }, Qt::QueuedConnection); + + m_usingCache = true; + emit usingCacheChanged(); + } else { + QMetaObject::invokeMethod(m_item, [path, this]() { + m_item->setProperty("source", QUrl::fromLocalFile(path)); + }, Qt::QueuedConnection); + + m_usingCache = false; + emit usingCacheChanged(); + } + }); + } +} + +QUrl CachingImageManager::cachePath() const { + return m_cachePath; +} + +bool CachingImageManager::usingCache() const { + return m_usingCache; +} + +QString CachingImageManager::sha256sum(const QString& path) const { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "CachingImageManager::sha256sum: failed to open" << path; + return ""; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} diff --git a/plugin/src/Caelestia/cachingimagemanager.hpp b/plugin/src/Caelestia/cachingimagemanager.hpp new file mode 100644 index 0000000..e2cbd41 --- /dev/null +++ b/plugin/src/Caelestia/cachingimagemanager.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include <qobject.h> +#include <qqmlintegration.h> +#include <QtQuick/QQuickItem> + +class CachingImageManager : public QObject { + Q_OBJECT; + QML_ELEMENT; + + Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED); + Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED); + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged); + Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged); + Q_PROPERTY(bool usingCache READ usingCache NOTIFY usingCacheChanged); + +public: + explicit CachingImageManager(QObject* parent = nullptr): QObject(parent) {}; + + [[nodiscard]] QQuickItem* item() const; + void setItem(QQuickItem* item); + + [[nodiscard]] QUrl cacheDir() const; + void setCacheDir(const QUrl& cacheDir); + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + [[nodiscard]] QUrl cachePath() const; + [[nodiscard]] bool usingCache() const; + +signals: + void itemChanged(); + void cacheDirChanged(); + + void pathChanged(); + void cachePathChanged(); + void usingCacheChanged(); + +private slots: + void handleStatusChanged(); + +private: + QQuickItem* m_item; + QUrl m_cacheDir; + + QString m_path; + QUrl m_cachePath; + bool m_usingCache; + + [[nodiscard]] qreal effectiveScale() const; + int effectiveWidth() const; + int effectiveHeight() const; + + [[nodiscard]] QString sha256sum(const QString& path) const; +}; diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp index e1319a4..76beb2b 100644 --- a/plugin/src/Caelestia/cutils.hpp +++ b/plugin/src/Caelestia/cutils.hpp @@ -1,11 +1,12 @@ #pragma once #include <qobject.h> +#include <qqmlintegration.h> #include <QtQuick/QQuickItem> class CUtils : public QObject { Q_OBJECT; - QML_NAMED_ELEMENT(CUtils); + QML_ELEMENT; QML_SINGLETON; public: |