File indexing completed on 2024-11-10 05:02:45

0001 /*
0002     SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "mediaproxy.h"
0008 
0009 #include "defaultwallpaper.h"
0010 
0011 #include <QBuffer>
0012 #include <QFileInfo>
0013 #include <QGuiApplication>
0014 #include <QMimeDatabase>
0015 #include <QMovie>
0016 #include <QScreen>
0017 #include <QUrlQuery>
0018 
0019 #include <KConfigGroup>
0020 #include <KIO/OpenUrlJob>
0021 #include <KNotificationJobUiDelegate>
0022 #include <KPackage/PackageLoader>
0023 
0024 #include <Plasma/Theme>
0025 
0026 #include "../finder/packagefinder.h"
0027 
0028 #include "debug.h"
0029 
0030 namespace
0031 {
0032 static const QString s_wallpaperPackageName = QStringLiteral("Wallpaper/Images");
0033 }
0034 
0035 MediaProxy::MediaProxy(QObject *parent)
0036     : QObject(parent)
0037     , m_targetSize(qGuiApp->primaryScreen()->size() * qGuiApp->primaryScreen()->devicePixelRatio())
0038     , m_isDarkColorScheme(isDarkColorScheme())
0039 {
0040     connect(&m_dirWatch, &KDirWatch::created, this, &MediaProxy::slotSourceFileUpdated);
0041 }
0042 
0043 void MediaProxy::classBegin()
0044 {
0045 }
0046 
0047 void MediaProxy::componentComplete()
0048 {
0049     // don't bother loading single image until all properties have settled
0050     // otherwise we would load a too small image (initial view size) just
0051     // to load the proper one afterwards etc etc
0052     m_ready = true;
0053 
0054     // Follow system color scheme
0055     connect(qGuiApp, &QGuiApplication::paletteChanged, this, &MediaProxy::slotSystemPaletteChanged);
0056 
0057     processSource();
0058 }
0059 
0060 QString MediaProxy::source() const
0061 {
0062     return m_source.toString();
0063 }
0064 
0065 void MediaProxy::setSource(const QString &url)
0066 {
0067     // New desktop has empty url
0068     if (url.isEmpty()) {
0069         if (!m_isDefaultSource) {
0070             useSingleImageDefaults();
0071             m_isDefaultSource = true;
0072         }
0073         return;
0074     }
0075 
0076     m_isDefaultSource = false;
0077 
0078     const QUrl sanitizedUrl = QUrl::fromUserInput(url);
0079     if (m_source == sanitizedUrl) {
0080         return;
0081     }
0082 
0083     if (!m_source.isEmpty()) {
0084         m_dirWatch.removeFile(m_source.toLocalFile());
0085     }
0086     m_source = sanitizedUrl;
0087     if (QFileInfo(m_source.toLocalFile()).isFile()) {
0088         m_dirWatch.addFile(m_source.toLocalFile());
0089     }
0090     Q_EMIT sourceChanged();
0091 
0092     m_providerType = Provider::Type::Unknown;
0093     processSource();
0094 }
0095 
0096 QUrl MediaProxy::modelImage() const
0097 {
0098     return m_modelImage;
0099 }
0100 
0101 QSize MediaProxy::targetSize() const
0102 {
0103     return m_targetSize;
0104 }
0105 
0106 void MediaProxy::setTargetSize(const QSize &size)
0107 {
0108     if (m_targetSize == size) {
0109         return;
0110     }
0111 
0112     m_targetSize = size;
0113     Q_EMIT targetSizeChanged(size);
0114 
0115     if (m_providerType == Provider::Type::Package) {
0116         // In case file format changes after size changes
0117         processSource();
0118     }
0119     if (m_providerType == Provider::Type::Image || m_backgroundType == BackgroundType::Type::AnimatedImage) {
0120         // When KPackage contains animated wallpapers, image provider is not used.
0121         Q_EMIT actualSizeChanged();
0122     }
0123 }
0124 
0125 Provider::Type MediaProxy::providerType() const
0126 {
0127     return m_providerType;
0128 }
0129 
0130 void MediaProxy::openModelImage()
0131 {
0132     QUrl url;
0133 
0134     switch (m_providerType) {
0135     case Provider::Type::Image: {
0136         url = m_modelImage;
0137         break;
0138     }
0139 
0140     case Provider::Type::Package: {
0141         auto package = KPackage::PackageLoader::self()->loadPackage(s_wallpaperPackageName);
0142         package.setPath(m_source.toLocalFile());
0143 
0144         url = findPreferredImageInPackage(package);
0145         break;
0146     }
0147 
0148     default:
0149         return;
0150     }
0151 
0152     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url);
0153     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0154     job->start();
0155 }
0156 
0157 void MediaProxy::useSingleImageDefaults()
0158 {
0159     m_source.clear(); // BUG 460692
0160 
0161     auto package = DefaultWallpaper::defaultWallpaperPackage();
0162 
0163     if (!package.isValid()) {
0164         return;
0165     }
0166 
0167     m_source = QUrl::fromLocalFile(package.path());
0168 
0169     PackageFinder::findPreferredImageInPackage(package, m_targetSize);
0170 
0171     // Make sure the image can be read, or there will be dead loops.
0172     if (m_source.isEmpty() || QImage(package.filePath("preferred")).isNull()) {
0173         return;
0174     }
0175 
0176     Q_EMIT sourceChanged();
0177 
0178     m_providerType = Provider::Type::Unknown;
0179     processSource(&package);
0180 }
0181 
0182 void MediaProxy::slotSystemPaletteChanged(const QPalette &palette)
0183 {
0184     if (m_providerType != Provider::Type::Package) {
0185         // Currently only KPackage supports adaptive wallpapers
0186         return;
0187     }
0188 
0189     const bool dark = isDarkColorScheme(palette);
0190 
0191     if (dark == m_isDarkColorScheme) {
0192         return;
0193     }
0194 
0195     m_isDarkColorScheme = dark;
0196 
0197     if (m_providerType == Provider::Type::Package) {
0198         // In case light and dark variants have different formats
0199         processSource(nullptr, true /* Immediately change the wallpaper */);
0200     }
0201 
0202     Q_EMIT colorSchemeChanged();
0203 }
0204 
0205 void MediaProxy::slotSourceFileUpdated(const QString &path)
0206 {
0207     Q_ASSERT(path == m_source.toLocalFile());
0208     if (m_providerType == Provider::Type::Unknown) {
0209         processSource();
0210     }
0211     Q_EMIT sourceFileUpdated();
0212 }
0213 
0214 bool MediaProxy::isDarkColorScheme(const QPalette &palette)
0215 {
0216     // 192 is from kcm_colors
0217     if (palette == QPalette()) {
0218         return qGray(qGuiApp->palette().window().color().rgb()) < 192;
0219     }
0220     return qGray(palette.window().color().rgb()) < 192;
0221 }
0222 
0223 QColor MediaProxy::getAccentColorFromMetaData(const KPackage::Package &package)
0224 {
0225     const QJsonObject metaData = package.metadata().rawData();
0226     const auto jsonIt = metaData.constFind(QStringLiteral("X-KDE-PlasmaImageWallpaper-AccentColor"));
0227     if (jsonIt == metaData.constEnd()) {
0228         return QColor();
0229     }
0230 
0231     QString colorString = QStringLiteral("transparent");
0232     const auto accentColorValue = jsonIt.value();
0233     switch (accentColorValue.type()) {
0234     case QJsonValue::String: {
0235         colorString = accentColorValue.toString();
0236         if (!colorString.isEmpty()) {
0237             break;
0238         }
0239         [[fallthrough]];
0240     }
0241 
0242     case QJsonValue::Object: {
0243         const QJsonObject accentColorDict = accentColorValue.toObject();
0244         if (isDarkColorScheme()) {
0245             const auto darkIt = accentColorDict.constFind(QStringLiteral("Dark"));
0246             if (darkIt != accentColorDict.constEnd()) {
0247                 colorString = darkIt.value().toString();
0248                 if (!colorString.isEmpty()) {
0249                     break;
0250                 }
0251             }
0252         }
0253 
0254         // Light color as fallback
0255         const auto lightIt = accentColorDict.constFind(QStringLiteral("Light"));
0256         if (lightIt != accentColorDict.constEnd()) {
0257             colorString = lightIt.value().toString();
0258             break;
0259         }
0260         [[fallthrough]];
0261     }
0262 
0263     default:
0264         qCWarning(IMAGEWALLPAPER, "Invalid value from \"X-KDE-PlasmaImageWallpaper-AccentColor\"");
0265         break;
0266     }
0267 
0268     return QColor(colorString);
0269 }
0270 
0271 void MediaProxy::determineBackgroundType(KPackage::Package *package)
0272 {
0273     QString filePath;
0274     if (package) {
0275         filePath = findPreferredImageInPackage(*package).toLocalFile();
0276     } else {
0277         filePath = m_source.toLocalFile();
0278     }
0279 
0280     QMimeDatabase db;
0281     const QString type = db.mimeTypeForFile(filePath).name();
0282 
0283     QBuffer dummyBuffer;
0284     dummyBuffer.open(QIODevice::ReadOnly);
0285     // Don't use QMovie::supportedFormats() as it loads all available image plugins
0286     const bool isAnimated = QImageReader(&dummyBuffer, QFileInfo(filePath).suffix().toLower().toLatin1()).supportsOption(QImageIOHandler::Animation);
0287 
0288     if (isAnimated) {
0289         // Derived from the suffix
0290         m_backgroundType = BackgroundType::Type::AnimatedImage;
0291     } else if (type.startsWith(QLatin1String("image/"))) {
0292         m_backgroundType = BackgroundType::Type::Image;
0293     } else {
0294         m_backgroundType = BackgroundType::Type::Unknown;
0295     }
0296 
0297     Q_EMIT backgroundTypeChanged();
0298 }
0299 
0300 void MediaProxy::determineProviderType()
0301 {
0302     QFileInfo info(m_source.toLocalFile());
0303 
0304     auto oldType = m_providerType;
0305 
0306     if (info.isFile()) {
0307         m_providerType = Provider::Type::Image;
0308     } else if (info.isDir()) {
0309         m_providerType = Provider::Type::Package;
0310     } else {
0311         m_providerType = Provider::Type::Unknown;
0312     }
0313 
0314     if (oldType != m_providerType) {
0315         Q_EMIT providerTypeChanged();
0316     }
0317 }
0318 
0319 void MediaProxy::processSource(KPackage::Package *package, bool doesBlockSignal)
0320 {
0321     if (!m_ready) {
0322         return;
0323     }
0324 
0325     if (m_providerType == Provider::Type::Unknown) {
0326         determineProviderType();
0327     }
0328 
0329     if (!package && m_providerType == Provider::Type::Package) {
0330         KPackage::Package _package = KPackage::PackageLoader::self()->loadPackage(s_wallpaperPackageName);
0331         _package.setPath(m_source.toLocalFile());
0332         determineBackgroundType(&_package);
0333         updateModelImage(&_package, doesBlockSignal);
0334     } else {
0335         determineBackgroundType(package);
0336         updateModelImage(package, doesBlockSignal);
0337     }
0338 }
0339 
0340 QUrl MediaProxy::findPreferredImageInPackage(KPackage::Package &package)
0341 {
0342     QUrl url;
0343 
0344     if (!package.isValid()) {
0345         return url;
0346     }
0347 
0348     PackageFinder::findPreferredImageInPackage(package, m_targetSize);
0349     url = package.fileUrl("preferred");
0350 
0351     if (isDarkColorScheme()) {
0352         const QUrl darkUrl = package.fileUrl("preferredDark");
0353 
0354         if (!darkUrl.isEmpty()) {
0355             url = darkUrl;
0356         }
0357     }
0358 
0359     return url;
0360 }
0361 
0362 void MediaProxy::updateModelImage(KPackage::Package *package, bool doesBlockSignal)
0363 {
0364     if (!m_ready) {
0365         return;
0366     }
0367 
0368     m_customColor = Qt::transparent;
0369 
0370     QUrl newRealSource;
0371 
0372     switch (m_providerType) {
0373     case Provider::Type::Image: {
0374         newRealSource = m_source;
0375         break;
0376     }
0377 
0378     case Provider::Type::Package: {
0379         // Get custom accent color from
0380 
0381         const QColor color(getAccentColorFromMetaData(*package));
0382         if (m_customColor != color && color.isValid() && color != Qt::transparent) {
0383             m_customColor = color;
0384             Q_EMIT customColorChanged();
0385         }
0386 
0387         if (m_backgroundType == BackgroundType::Type::AnimatedImage) {
0388             // Is an animated image
0389             newRealSource = findPreferredImageInPackage(*package);
0390             break;
0391         }
0392 
0393         // Use a custom image provider
0394         QUrl composedUrl(QStringLiteral("image://package/get"));
0395 
0396         QUrlQuery urlQuery(composedUrl);
0397         urlQuery.addQueryItem(QStringLiteral("dir"), m_source.toLocalFile());
0398         // To make modelImageChaged work
0399         urlQuery.addQueryItem(QStringLiteral("targetWidth"), QString::number(m_targetSize.width()));
0400         urlQuery.addQueryItem(QStringLiteral("targetHeight"), QString::number(m_targetSize.height()));
0401         urlQuery.addQueryItem(QStringLiteral("darkMode"), QString::number(isDarkColorScheme() ? 1 : 0));
0402 
0403         composedUrl.setQuery(urlQuery);
0404         newRealSource = composedUrl;
0405         break;
0406     }
0407 
0408     case Provider::Type::Unknown:
0409     default:
0410         return;
0411     }
0412 
0413     if (m_modelImage == newRealSource) {
0414         return;
0415     }
0416 
0417     m_modelImage = newRealSource;
0418     if (!doesBlockSignal) {
0419         Q_EMIT modelImageChanged();
0420     }
0421 }
0422 
0423 void MediaProxy::updateModelImageWithoutSignal(KPackage::Package *package)
0424 {
0425     updateModelImage(package, true);
0426 }