C++ / fmt (pull_request) Successful in 3s
JS/TS / lint (pull_request) Successful in 18s
JS/TS / fmt (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 28s
Python / lint (pull_request) Successful in 30s
Python / test (pull_request) Successful in 1m5s
Python / typecheck (pull_request) Successful in 1m24s
Rust / fmt (pull_request) Successful in 31s
C++ / build (pull_request) Successful in 2m21s
Rust / build (pull_request) Successful in 1m47s
Python / buildcheck (pull_request) Successful in 2m18s
Rust / clippy (pull_request) Successful in 1m12s
C++ / clang-tidy (pull_request) Successful in 4m3s
354 lines
8.7 KiB
C++
354 lines
8.7 KiB
C++
#include "wallpaperimage.hpp"
|
|
|
|
#include <QStandardPaths>
|
|
#include <QCryptographicHash>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QImageReader>
|
|
#include <QtConcurrent>
|
|
#include <QQuickWindow>
|
|
|
|
namespace ZShell::internal {
|
|
|
|
QThreadPool* WallpaperImage::loadPool() {
|
|
static QThreadPool* pool = [] {
|
|
auto* p = new QThreadPool();
|
|
p->setMaxThreadCount(4);
|
|
return p;
|
|
}();
|
|
return pool;
|
|
}
|
|
|
|
WallpaperImage::WallpaperImage(QQuickItem* parent) : QQuickItem(parent) {
|
|
setFlag(ItemHasContents, true);
|
|
|
|
m_loadDebounceTimer.setSingleShot(true);
|
|
connect(
|
|
&m_loadDebounceTimer,
|
|
&QTimer::timeout,
|
|
this,
|
|
&WallpaperImage::beginLoad);
|
|
|
|
connect(
|
|
&m_imageWatcher,
|
|
&QFutureWatcher<QImage>::finished,
|
|
this,
|
|
&WallpaperImage::handleImageLoaded);
|
|
}
|
|
|
|
WallpaperImage::~WallpaperImage() {
|
|
m_requestGeneration->fetch_add(1, std::memory_order_relaxed);
|
|
m_imageWatcher.disconnect();
|
|
|
|
if (m_texture) delete m_texture;
|
|
if (m_previousTexture) delete m_previousTexture;
|
|
}
|
|
|
|
void WallpaperImage::setStatus(const Status s) {
|
|
if (m_status == s) return;
|
|
m_status = s;
|
|
emit statusChanged();
|
|
}
|
|
|
|
void WallpaperImage::setSource(const QUrl& source) {
|
|
if (m_source == source) return;
|
|
m_source = source;
|
|
emit sourceChanged();
|
|
|
|
setStatus(Loading);
|
|
m_loadDebounceTimer.start(80);
|
|
}
|
|
|
|
void WallpaperImage::setScreenResolution(const QSize& screenResolution) {
|
|
if (m_screenResolution == screenResolution) return;
|
|
m_screenResolution = screenResolution;
|
|
emit screenResolutionChanged();
|
|
beginLoad();
|
|
}
|
|
|
|
void WallpaperImage::setZoom(qreal zoom) {
|
|
if (qFuzzyCompare(m_zoom, zoom)) return;
|
|
m_zoom = zoom;
|
|
emit zoomChanged();
|
|
update();
|
|
}
|
|
|
|
void WallpaperImage::setFadeDuration(int ms) {
|
|
if (ms < 0) ms = 0;
|
|
if (m_fadeDuration == ms) return;
|
|
m_fadeDuration = ms;
|
|
emit fadeDurationChanged();
|
|
}
|
|
|
|
void WallpaperImage::setCropX(qreal x) {
|
|
if (qFuzzyCompare(m_cropX, x)) return;
|
|
m_cropX = x;
|
|
emit cropXChanged();
|
|
update();
|
|
}
|
|
|
|
void WallpaperImage::setCropY(qreal y) {
|
|
if (qFuzzyCompare(m_cropY, y)) return;
|
|
m_cropY = y;
|
|
emit cropYChanged();
|
|
update();
|
|
}
|
|
|
|
void WallpaperImage::setCropWidth(qreal w) {
|
|
if (w <= 0.0) w = 1.0;
|
|
if (qFuzzyCompare(m_cropWidth, w)) return;
|
|
m_cropWidth = w;
|
|
emit cropWidthChanged();
|
|
update();
|
|
}
|
|
|
|
void WallpaperImage::setCropHeight(qreal h) {
|
|
if (h <= 0.0) h = 1.0;
|
|
if (qFuzzyCompare(m_cropHeight, h)) return;
|
|
m_cropHeight = h;
|
|
emit cropHeightChanged();
|
|
update();
|
|
}
|
|
|
|
QString WallpaperImage::getCacheFilePath() const {
|
|
if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString();
|
|
|
|
QString cachePath =
|
|
QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) +
|
|
"/zshell/imagecache";
|
|
QDir().mkpath(cachePath);
|
|
|
|
QString id = m_source.toString() + "_" +
|
|
QString::number(m_screenResolution.width()) + "x" +
|
|
QString::number(m_screenResolution.height());
|
|
QByteArray hash =
|
|
QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();
|
|
|
|
return cachePath + "/" + hash + ".png";
|
|
}
|
|
|
|
void WallpaperImage::beginLoad() {
|
|
if (m_source.isEmpty()) {
|
|
setStatus(Null);
|
|
return;
|
|
}
|
|
|
|
const quint64 requestId =
|
|
m_requestGeneration->fetch_add(1, std::memory_order_relaxed) + 1;
|
|
|
|
QString cacheFile = getCacheFilePath();
|
|
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile()
|
|
: m_source.toString();
|
|
if (sourceFile.startsWith("qrc:/")) sourceFile = sourceFile.mid(3);
|
|
|
|
QSize targetRes = m_screenResolution;
|
|
auto generation = m_requestGeneration;
|
|
|
|
QFuture<QImage> future = QtConcurrent::run(
|
|
loadPool(),
|
|
[sourceFile, cacheFile, targetRes, requestId, generation]() -> QImage {
|
|
auto stale = [&] {
|
|
return generation->load(std::memory_order_relaxed) != requestId;
|
|
};
|
|
|
|
if (stale()) return QImage();
|
|
|
|
if (!targetRes.isEmpty() && !cacheFile.isEmpty() &&
|
|
QFileInfo::exists(cacheFile)) {
|
|
QImage cached(cacheFile);
|
|
if (!cached.isNull()) return cached;
|
|
}
|
|
|
|
if (stale()) return QImage();
|
|
|
|
QImage original;
|
|
QImageReader reader(sourceFile);
|
|
if (!targetRes.isEmpty() &&
|
|
reader.supportsOption(QImageIOHandler::Size)) {
|
|
QSize actual = reader.size();
|
|
if (actual.isValid() &&
|
|
(actual.width() > targetRes.width() ||
|
|
actual.height() > targetRes.height())) {
|
|
QSize scaledSize =
|
|
actual.scaled(targetRes, Qt::KeepAspectRatioByExpanding);
|
|
reader.setScaledSize(scaledSize);
|
|
}
|
|
original = reader.read();
|
|
} else {
|
|
original = QImage(sourceFile);
|
|
}
|
|
if (original.isNull()) return QImage();
|
|
|
|
if (stale()) return QImage();
|
|
|
|
if (targetRes.isEmpty()) return original;
|
|
|
|
QImage scaled = (original.width() > targetRes.width() ||
|
|
original.height() > targetRes.height())
|
|
? original.scaled(
|
|
targetRes,
|
|
Qt::KeepAspectRatioByExpanding,
|
|
Qt::SmoothTransformation)
|
|
: original;
|
|
|
|
if (stale()) return scaled;
|
|
|
|
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
|
|
return scaled;
|
|
});
|
|
|
|
m_imageWatcher.setFuture(future);
|
|
}
|
|
|
|
void WallpaperImage::handleImageLoaded() {
|
|
QImage result = m_imageWatcher.result();
|
|
if (result.isNull()) {
|
|
setStatus(Error);
|
|
return;
|
|
}
|
|
|
|
m_pendingImage = result;
|
|
m_hasPending = true;
|
|
setStatus(Ready);
|
|
update();
|
|
}
|
|
|
|
QRectF WallpaperImage::computeSourceRect(QSGTexture* texture) const {
|
|
QSize texSize = texture->textureSize();
|
|
|
|
qreal cW = m_cropWidth / m_zoom;
|
|
qreal cH = m_cropHeight / m_zoom;
|
|
|
|
QRectF reqRect(
|
|
m_cropX * texSize.width(),
|
|
m_cropY * texSize.height(),
|
|
cW * texSize.width(),
|
|
cH * texSize.height());
|
|
|
|
QRectF bounds = boundingRect();
|
|
if (bounds.isEmpty() || reqRect.isEmpty()) return reqRect;
|
|
|
|
qreal targetRatio = bounds.width() / bounds.height();
|
|
qreal reqRatio = reqRect.width() / reqRect.height();
|
|
|
|
QRectF sourceRect = reqRect;
|
|
|
|
if (reqRatio > targetRatio) {
|
|
qreal newWidth = reqRect.height() * targetRatio;
|
|
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
|
|
sourceRect.setX(reqRect.x() + xOffset);
|
|
sourceRect.setWidth(newWidth);
|
|
} else if (reqRatio < targetRatio) {
|
|
qreal newHeight = reqRect.width() / targetRatio;
|
|
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
|
|
sourceRect.setY(reqRect.y() + yOffset);
|
|
sourceRect.setHeight(newHeight);
|
|
}
|
|
|
|
return sourceRect;
|
|
}
|
|
|
|
void WallpaperImage::configureImageNode(
|
|
QSGImageNode* node, QSGTexture* texture) {
|
|
node->setTexture(texture);
|
|
node->setRect(boundingRect());
|
|
node->setFiltering(QSGTexture::Linear);
|
|
|
|
QRectF sourceRect = computeSourceRect(texture);
|
|
node->setSourceRect(sourceRect);
|
|
|
|
if (texture == m_texture) {
|
|
QSize texSize = texture->textureSize();
|
|
QRectF normalizedActual(
|
|
sourceRect.x() / texSize.width(),
|
|
sourceRect.y() / texSize.height(),
|
|
sourceRect.width() / texSize.width(),
|
|
sourceRect.height() / texSize.height());
|
|
|
|
bool changed = false;
|
|
auto updateIfChanged = [&](qreal& dst, qreal value) {
|
|
if (!qFuzzyCompare(dst, value)) {
|
|
dst = value;
|
|
changed = true;
|
|
}
|
|
};
|
|
updateIfChanged(m_actualCropX, normalizedActual.x());
|
|
updateIfChanged(m_actualCropY, normalizedActual.y());
|
|
updateIfChanged(m_actualCropWidth, normalizedActual.width());
|
|
updateIfChanged(m_actualCropHeight, normalizedActual.height());
|
|
if (changed) emit actualCropChanged();
|
|
}
|
|
}
|
|
|
|
QSGNode* WallpaperImage::updatePaintNode(
|
|
QSGNode* oldNode, UpdatePaintNodeData*) {
|
|
Q_UNUSED(oldNode);
|
|
|
|
if (m_hasPending) {
|
|
if (m_previousTexture) {
|
|
delete m_previousTexture;
|
|
m_previousTexture = nullptr;
|
|
}
|
|
m_previousTexture = m_texture;
|
|
m_texture = window()->createTextureFromImage(
|
|
m_pendingImage, QQuickWindow::TextureHasAlphaChannel);
|
|
m_pendingImage = QImage();
|
|
m_hasPending = false;
|
|
|
|
if (m_previousTexture && m_fadeDuration > 0) {
|
|
m_fading = true;
|
|
m_fadeTimer.restart();
|
|
} else {
|
|
if (m_previousTexture) {
|
|
delete m_previousTexture;
|
|
m_previousTexture = nullptr;
|
|
}
|
|
m_fading = false;
|
|
}
|
|
}
|
|
|
|
if (!m_texture) {
|
|
return m_rootNode;
|
|
}
|
|
|
|
if (!m_rootNode) {
|
|
m_rootNode = new QSGNode();
|
|
|
|
m_newOpacityNode = new QSGOpacityNode();
|
|
m_newImageNode = window()->createImageNode();
|
|
m_newOpacityNode->appendChildNode(m_newImageNode);
|
|
m_rootNode->appendChildNode(m_newOpacityNode);
|
|
|
|
m_oldOpacityNode = new QSGOpacityNode();
|
|
m_oldImageNode = window()->createImageNode();
|
|
m_oldOpacityNode->appendChildNode(m_oldImageNode);
|
|
m_rootNode->appendChildNode(m_oldOpacityNode);
|
|
}
|
|
|
|
configureImageNode(m_newImageNode, m_texture);
|
|
m_newOpacityNode->setOpacity(1.0);
|
|
|
|
if (m_fading && m_previousTexture) {
|
|
qreal progress =
|
|
qBound(0.0, qreal(m_fadeTimer.elapsed()) / m_fadeDuration, 1.0);
|
|
|
|
configureImageNode(m_oldImageNode, m_previousTexture);
|
|
m_oldOpacityNode->setOpacity(1.0 - progress);
|
|
|
|
if (progress >= 1.0) {
|
|
m_fading = false;
|
|
delete m_previousTexture;
|
|
m_previousTexture = nullptr;
|
|
m_oldOpacityNode->setOpacity(0.0);
|
|
} else {
|
|
update();
|
|
}
|
|
} else {
|
|
m_oldOpacityNode->setOpacity(0.0);
|
|
}
|
|
|
|
return m_rootNode;
|
|
}
|
|
|
|
} // namespace ZShell::internal
|