#include "wallpaperimage.hpp" #include #include #include #include #include #include #include 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::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 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