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

0001 /*
0002     SPDX-FileCopyrightText: 2007 Paolo Capriotti <p.capriotti@gmail.com>
0003     SPDX-FileCopyrightText: 2007 Aaron Seigo <aseigo@kde.org>
0004     SPDX-FileCopyrightText: 2008 Petri Damsten <damu@iki.fi>
0005     SPDX-FileCopyrightText: 2008 Alexis Ménard <darktears31@gmail.com>
0006     SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas@kde.org>
0007     SPDX-FileCopyrightText: 2015 Kai Uwe Broulik <kde@privat.broulik.de>
0008     SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de>
0009 
0010     SPDX-License-Identifier: GPL-2.0-or-later
0011 */
0012 
0013 #include "imagebackend.h"
0014 
0015 #include <math.h>
0016 
0017 #include <QDir>
0018 #include <QFileInfo>
0019 #include <QGuiApplication>
0020 #include <QImageReader>
0021 #include <QMimeDatabase>
0022 #include <QScreen>
0023 #include <QStandardPaths>
0024 
0025 #include <KLocalizedString>
0026 
0027 #include "model/imageproxymodel.h"
0028 #include "slidefiltermodel.h"
0029 #include "slidemodel.h"
0030 
0031 ImageBackend::ImageBackend(QObject *parent)
0032     : QObject(parent)
0033     , m_targetSize(qGuiApp->primaryScreen()->size() * qGuiApp->primaryScreen()->devicePixelRatio())
0034 {
0035     connect(&m_timer, &QTimer::timeout, this, &ImageBackend::nextSlide);
0036 }
0037 
0038 ImageBackend::~ImageBackend()
0039 {
0040 }
0041 
0042 void ImageBackend::classBegin()
0043 {
0044 }
0045 
0046 void ImageBackend::componentComplete()
0047 {
0048     m_ready = true;
0049 
0050     // MediaProxy will handle SingleImage case
0051     if (m_usedInConfig) {
0052         ensureWallpaperModel();
0053         ensureSlideshowModel();
0054     } else {
0055         startSlideshow();
0056     }
0057 }
0058 
0059 QString ImageBackend::image() const
0060 {
0061     return m_image.toString();
0062 }
0063 
0064 void ImageBackend::setImage(const QString &url)
0065 {
0066     if (url.isEmpty() || m_image == QUrl::fromUserInput(url)) {
0067         return;
0068     }
0069 
0070     m_image = QUrl::fromUserInput(url);
0071     Q_EMIT imageChanged();
0072 }
0073 
0074 ImageBackend::RenderingMode ImageBackend::renderingMode() const
0075 {
0076     return m_mode;
0077 }
0078 
0079 void ImageBackend::setRenderingMode(RenderingMode mode)
0080 {
0081     if (mode == m_mode) {
0082         return;
0083     }
0084 
0085     m_mode = mode;
0086     Q_EMIT renderingModeChanged();
0087 
0088     startSlideshow();
0089 }
0090 
0091 SortingMode::Mode ImageBackend::slideshowMode() const
0092 {
0093     return m_slideshowMode;
0094 }
0095 
0096 void ImageBackend::setSlideshowMode(SortingMode::Mode slideshowMode)
0097 {
0098     if (slideshowMode == m_slideshowMode) {
0099         return;
0100     }
0101 
0102     m_slideshowMode = slideshowMode;
0103 
0104     startSlideshow();
0105 }
0106 
0107 bool ImageBackend::slideshowFoldersFirst() const
0108 {
0109     return m_slideshowFoldersFirst;
0110 }
0111 
0112 void ImageBackend::setSlideshowFoldersFirst(bool slideshowFoldersFirst)
0113 {
0114     if (slideshowFoldersFirst == m_slideshowFoldersFirst) {
0115         return;
0116     }
0117 
0118     m_slideshowFoldersFirst = slideshowFoldersFirst;
0119 
0120     startSlideshow();
0121 }
0122 
0123 QSize ImageBackend::targetSize() const
0124 {
0125     return m_targetSize.value();
0126 }
0127 
0128 void ImageBackend::setTargetSize(const QSize &size)
0129 {
0130     Q_ASSERT(size.isValid());
0131     m_targetSize = size;
0132 }
0133 
0134 QAbstractItemModel *ImageBackend::wallpaperModel() const
0135 {
0136     Q_ASSERT(m_mode == SingleImage);
0137     return m_model;
0138 }
0139 
0140 void ImageBackend::ensureWallpaperModel()
0141 {
0142     if (m_model || m_mode != SingleImage) {
0143         return;
0144     }
0145 
0146     m_model = new ImageProxyModel({}, QBindable<QSize>(&m_targetSize), QBindable<bool>(&m_usedInConfig), this);
0147     m_loading.setBinding(m_model->loading().makeBinding());
0148 
0149     Q_EMIT wallpaperModelChanged();
0150 }
0151 
0152 void ImageBackend::ensureSlideshowModel()
0153 {
0154     if (m_slideshowModel || m_mode != SlideShow) {
0155         return;
0156     }
0157 
0158     m_slideshowModel = new SlideModel(QBindable<QSize>(&m_targetSize), QBindable<bool>(&m_usedInConfig), this);
0159     m_slideshowModel->setUncheckedSlides(m_uncheckedSlides);
0160     m_loading.setBinding(m_slideshowModel->loading().makeBinding());
0161 
0162     m_slideFilterModel = new SlideFilterModel(QBindable<bool>(&m_usedInConfig), //
0163                                               QBindable<SortingMode::Mode>(&m_slideshowMode), //
0164                                               QBindable<bool>(&m_slideshowFoldersFirst), //
0165                                               this);
0166     // setSourceModel(...) must be done in backgroundsFound() to generate a complete random order
0167 
0168     connect(this, &ImageBackend::uncheckedSlidesChanged, m_slideFilterModel, &SlideFilterModel::invalidateFilter);
0169     connect(m_slideshowModel, &SlideModel::dataChanged, this, &ImageBackend::slotSlideModelDataChanged);
0170 
0171     if (m_usedInConfig) {
0172         // When not used in config, slide paths are set in startSlideshow()
0173         m_slideshowModel->setSlidePaths(m_slidePaths);
0174         if (m_slideshowModel->loading().value()) {
0175             connect(m_slideshowModel, &SlideModel::done, this, &ImageBackend::backgroundsFound);
0176         } else {
0177             // In case it loads immediately
0178             m_slideFilterModel->setSourceModel(m_slideshowModel);
0179         }
0180     }
0181 
0182     Q_EMIT slideFilterModelChanged();
0183 }
0184 
0185 void ImageBackend::saveCurrentWallpaper()
0186 {
0187     if (!m_ready || m_usedInConfig || m_mode != RenderingMode::SlideShow || m_configMap.isNull() || !m_image.isValid()) {
0188         return;
0189     }
0190 
0191     QMetaObject::invokeMethod(this, "writeImageConfig", Qt::QueuedConnection, Q_ARG(QString, m_image.toString()));
0192 }
0193 
0194 QAbstractItemModel *ImageBackend::slideFilterModel() const
0195 {
0196     Q_ASSERT(m_mode == SlideShow);
0197     return m_slideFilterModel;
0198 }
0199 
0200 int ImageBackend::slideTimer() const
0201 {
0202     return m_delay;
0203 }
0204 
0205 void ImageBackend::setSlideTimer(int time)
0206 {
0207     if (time == m_delay) {
0208         return;
0209     }
0210 
0211     m_delay = time;
0212     Q_EMIT slideTimerChanged();
0213 
0214     startSlideshow();
0215 }
0216 
0217 QStringList ImageBackend::slidePaths() const
0218 {
0219     return m_slidePaths;
0220 }
0221 
0222 void ImageBackend::setSlidePaths(const QStringList &slidePaths)
0223 {
0224     if (slidePaths == m_slidePaths) {
0225         return;
0226     }
0227 
0228     m_slidePaths = slidePaths;
0229     m_slidePaths.removeAll(QString());
0230 
0231     if (!m_slidePaths.isEmpty()) {
0232         // Replace 'preferred://wallpaperlocations' with real paths
0233         const auto it = std::remove_if(m_slidePaths.begin(), m_slidePaths.end(), [](const QString &path) {
0234             return path == QLatin1String("preferred://wallpaperlocations");
0235         });
0236 
0237         if (it != m_slidePaths.end()) {
0238             m_slidePaths.erase(it, m_slidePaths.end());
0239             m_slidePaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/"), QStandardPaths::LocateDirectory);
0240         }
0241     }
0242     if (!m_usedInConfig) {
0243         startSlideshow();
0244     } else if (m_slideshowModel) {
0245         // When used in config, m_slideshowModel can be nullptr when the image wallpaper is being used.
0246         m_slideshowModel->setSlidePaths(m_slidePaths);
0247     }
0248     Q_EMIT slidePathsChanged();
0249 }
0250 
0251 bool ImageBackend::addSlidePath(const QUrl &url)
0252 {
0253     Q_ASSERT(m_mode == SlideShow);
0254     if (url.isEmpty()) {
0255         return false;
0256     }
0257 
0258     QString path = url.toLocalFile();
0259 
0260     // If path is a file, use its parent folder.
0261     const QFileInfo info(path);
0262 
0263     if (info.isFile()) {
0264         path = info.dir().absolutePath();
0265     }
0266 
0267     const QStringList results = m_slideshowModel->addDirs({path});
0268 
0269     if (results.empty()) {
0270         return false;
0271     }
0272 
0273     m_slidePaths.append(results);
0274     Q_EMIT slidePathsChanged();
0275 
0276     return true;
0277 }
0278 
0279 void ImageBackend::removeSlidePath(const QString &path)
0280 {
0281     Q_ASSERT(m_mode == SlideShow);
0282 
0283     /* BUG 461003 check path is in the config*/
0284     m_slideshowModel->removeDir(path);
0285 
0286     if (m_slidePaths.removeOne(path)) {
0287         Q_EMIT slidePathsChanged();
0288     }
0289 }
0290 
0291 void ImageBackend::startSlideshow()
0292 {
0293     if (!m_ready || m_usedInConfig || m_mode != SlideShow || m_pauseSlideshow) {
0294         return;
0295     }
0296     // populate background list
0297     m_timer.stop();
0298     ensureSlideshowModel();
0299     m_slideFilterModel->setSourceModel(nullptr);
0300     connect(m_slideshowModel, &SlideModel::done, this, &ImageBackend::backgroundsFound);
0301     m_slideshowModel->setSlidePaths(m_slidePaths);
0302     // TODO: what would be cool: paint on the wallpaper itself a busy widget and perhaps some text
0303     // about loading wallpaper slideshow while the thread runs
0304 }
0305 
0306 void ImageBackend::backgroundsFound()
0307 {
0308     disconnect(m_slideshowModel, &SlideModel::done, this, nullptr);
0309 
0310     // setSourceModel must be called after the model is loaded to generate a complete random order
0311     Q_ASSERT(!m_slideFilterModel->sourceModel());
0312     m_slideFilterModel->setSourceModel(m_slideshowModel);
0313 
0314     if (m_slideFilterModel->rowCount() == 0 || m_usedInConfig) {
0315         return;
0316     }
0317 
0318     // start slideshow
0319     m_slideFilterModel->sort(0);
0320     m_currentSlide = m_configMap.isNull() || m_slideshowMode == SortingMode::Random
0321         ? -1
0322         : m_slideFilterModel->indexOf(m_configMap->value(QStringLiteral("Image")).toString()) - 1;
0323     nextSlide();
0324 }
0325 
0326 QString ImageBackend::nameFilters() const
0327 {
0328     QStringList imageGlobPatterns;
0329     QMimeDatabase db;
0330     const auto supportedMimeTypes = QImageReader::supportedMimeTypes();
0331     for (const QByteArray &mimeType : supportedMimeTypes) {
0332         QMimeType mime(db.mimeTypeForName(QString::fromLatin1(mimeType)));
0333         imageGlobPatterns << mime.globPatterns();
0334     }
0335     // i18n people, this isn't a "word puzzle". there is a specific string format for QFileDialog::setNameFilters
0336     return i18n("Image Files") + QLatin1String(" (") + imageGlobPatterns.join(QLatin1Char(' ')) + QLatin1Char(')');
0337 }
0338 
0339 QQmlPropertyMap *ImageBackend::configMap() const
0340 {
0341     return m_configMap.data();
0342 }
0343 
0344 void ImageBackend::setConfigMap(QQmlPropertyMap *configMap)
0345 {
0346     if (configMap == m_configMap.data()) {
0347         return;
0348     }
0349 
0350     m_configMap = configMap;
0351     Q_EMIT configMapChanged();
0352 
0353     if (!m_configMap.isNull()) {
0354         Q_ASSERT(m_configMap->contains(QStringLiteral("Image")));
0355     }
0356 
0357     connect(m_configMap, &QQmlPropertyMap::valueChanged, this, [this](const QString &key, const QVariant & /* value */) {
0358         if (key == QStringLiteral("Image")) {
0359             Q_EMIT configMapChanged();
0360         }
0361     });
0362 
0363     saveCurrentWallpaper();
0364 }
0365 
0366 QString ImageBackend::addUsersWallpaper(const QUrl &url)
0367 {
0368     Q_ASSERT(m_mode == SingleImage);
0369     ensureWallpaperModel(); // The model is not created by default when used in desktop
0370     auto results = m_model->addBackground(url.isLocalFile() ? url.toLocalFile() : url.toString());
0371 
0372     if (!m_usedInConfig) {
0373         m_model->commitAddition();
0374         m_model->deleteLater();
0375         m_model = nullptr;
0376     }
0377 
0378     if (results.empty()) {
0379         return QString();
0380     }
0381 
0382     Q_EMIT settingsChanged();
0383 
0384     return results.at(0);
0385 }
0386 
0387 void ImageBackend::nextSlide()
0388 {
0389     const int rowCount = m_slideFilterModel->rowCount();
0390 
0391     if (!m_ready || m_usedInConfig || rowCount == 0) {
0392         return;
0393     }
0394     int previousSlide = m_currentSlide;
0395     QString previousPath;
0396     if (previousSlide >= 0) {
0397         previousPath = m_slideFilterModel->index(m_currentSlide, 0).data(ImageRoles::PackageNameRole).toString();
0398     }
0399     if (m_currentSlide >= rowCount - 1 /* ">" in case the last wallpaper is deleted before */ || m_currentSlide < 0) {
0400         m_currentSlide = 0;
0401     } else {
0402         m_currentSlide += 1;
0403     }
0404     // We are starting again - avoid having the same random order when we restart the slideshow
0405     if (m_slideshowMode == SortingMode::Random && m_currentSlide == 0) {
0406         m_slideFilterModel->invalidate();
0407     }
0408     QString next = m_slideFilterModel->index(m_currentSlide, 0).data(ImageRoles::PackageNameRole).toString();
0409     // And  avoid showing the same picture twice
0410     if (previousSlide == rowCount - 1 && previousPath == next && rowCount > 1) {
0411         m_currentSlide += 1;
0412         next = m_slideFilterModel->index(m_currentSlide, 0).data(ImageRoles::PackageNameRole).toString();
0413     }
0414     m_timer.stop();
0415     m_timer.start(m_delay * 1000);
0416     if (next.isEmpty()) {
0417         m_image = QUrl::fromLocalFile(previousPath);
0418     } else {
0419         m_image = QUrl::fromLocalFile(next);
0420         Q_EMIT imageChanged();
0421     }
0422 
0423     saveCurrentWallpaper();
0424 }
0425 
0426 void ImageBackend::slotSlideModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
0427 {
0428     Q_UNUSED(bottomRight);
0429 
0430     if (!topLeft.isValid()) {
0431         return;
0432     }
0433 
0434     if (roles.contains(ImageRoles::ToggleRole)) {
0435         if (topLeft.data(ImageRoles::ToggleRole).toBool()) {
0436             m_uncheckedSlides.removeOne(topLeft.data(ImageRoles::PackageNameRole).toString());
0437         } else {
0438             m_uncheckedSlides.append(topLeft.data(ImageRoles::PackageNameRole).toString());
0439         }
0440 
0441         Q_EMIT uncheckedSlidesChanged();
0442     }
0443 }
0444 
0445 QStringList ImageBackend::uncheckedSlides() const
0446 {
0447     return m_uncheckedSlides;
0448 }
0449 
0450 void ImageBackend::setUncheckedSlides(const QStringList &uncheckedSlides)
0451 {
0452     if (uncheckedSlides == m_uncheckedSlides) {
0453         return;
0454     }
0455     m_uncheckedSlides = uncheckedSlides;
0456 
0457     if (m_slideshowModel) {
0458         m_slideshowModel->setUncheckedSlides(m_uncheckedSlides);
0459     }
0460 
0461     Q_EMIT uncheckedSlidesChanged();
0462     startSlideshow();
0463 }
0464 
0465 bool ImageBackend::pauseSlideshow() const
0466 {
0467     return m_pauseSlideshow;
0468 }
0469 
0470 void ImageBackend::setPauseSlideshow(bool pauseSlideshow)
0471 {
0472     if (m_pauseSlideshow == pauseSlideshow) {
0473         return;
0474     }
0475 
0476     m_pauseSlideshow = pauseSlideshow;
0477     Q_EMIT pauseSlideshowChanged();
0478 
0479     if (!m_slideFilterModel) {
0480         return;
0481     }
0482 
0483     if (pauseSlideshow && m_timer.isActive()) {
0484         // Pause timer and store the remaining time
0485         m_remainingTime = m_timer.remainingTimeAsDuration();
0486         m_timer.stop();
0487     } else if (!pauseSlideshow && !m_timer.isActive()) {
0488         if (m_slideFilterModel->rowCount() > 0) {
0489             // Resume from the last point
0490             m_timer.start(m_remainingTime.value_or(std::chrono::seconds(m_delay)));
0491             m_remainingTime.reset();
0492         } else {
0493             // Start a new slideshow
0494             startSlideshow();
0495         }
0496     }
0497 }
0498 
0499 bool ImageBackend::loading() const
0500 {
0501     return m_loading;
0502 }