/* SPDX-FileCopyrightText: 2007 Paolo Capriotti SPDX-FileCopyrightText: 2007 Aaron Seigo SPDX-FileCopyrightText: 2008 Petri Damsten SPDX-FileCopyrightText: 2008 Alexis Ménard SPDX-FileCopyrightText: 2014 Sebastian Kügler SPDX-FileCopyrightText: 2015 Kai Uwe Broulik SPDX-FileCopyrightText: 2019 David Redondo SPDX-License-Identifier: GPL-2.0-or-later */ #include "imagebackend.h" #include #include #include #include #include #include #include #include #include #include #include "finder/suffixcheck.h" #include "model/imageproxymodel.h" #include "slidefiltermodel.h" #include "slidemodel.h" ImageBackend::ImageBackend(QObject *parent) : QObject(parent) , m_targetSize(qGuiApp->primaryScreen()->size() * qGuiApp->primaryScreen()->devicePixelRatio()) { m_timer.setSingleShot(true); m_timer.setTimerType(Qt::PreciseTimer); connect(&m_timer, &QTimer::timeout, this, &ImageBackend::nextSlide); } ImageBackend::~ImageBackend() { } void ImageBackend::classBegin() { } void ImageBackend::componentComplete() { m_ready = true; // MediaProxy will handle SingleImage case if (m_usedInConfig) { ensureWallpaperModel(); ensureSlideshowModel(); } else { startSlideshow(); } } QString ImageBackend::image() const { return m_image.toString(); } void ImageBackend::setImage(const QString &url) { if (url.isEmpty() || m_image == QUrl::fromUserInput(url)) { return; } m_image = QUrl::fromUserInput(url); Q_EMIT imageChanged(); } ImageBackend::RenderingMode ImageBackend::renderingMode() const { return m_mode; } void ImageBackend::setRenderingMode(RenderingMode mode) { if (mode == m_mode) { return; } m_mode = mode; if (m_ready) { if (m_usedInConfig) { ensureWallpaperModel(); ensureSlideshowModel(); } else { startSlideshow(); } } Q_EMIT renderingModeChanged(); } SortingMode::Mode ImageBackend::slideshowMode() const { return m_slideshowMode; } void ImageBackend::setSlideshowMode(SortingMode::Mode slideshowMode) { if (slideshowMode == m_slideshowMode) { return; } m_slideshowMode = slideshowMode; startSlideshow(); } bool ImageBackend::slideshowFoldersFirst() const { return m_slideshowFoldersFirst; } void ImageBackend::setSlideshowFoldersFirst(bool slideshowFoldersFirst) { if (slideshowFoldersFirst == m_slideshowFoldersFirst) { return; } m_slideshowFoldersFirst = slideshowFoldersFirst; startSlideshow(); } QSize ImageBackend::targetSize() const { return m_targetSize.value(); } void ImageBackend::setTargetSize(const QSize &size) { Q_ASSERT(size.isValid()); m_targetSize = size; } QAbstractItemModel *ImageBackend::wallpaperModel() const { Q_ASSERT(m_mode == SingleImage); return m_model; } void ImageBackend::ensureWallpaperModel() { if (m_model || m_mode != SingleImage) { return; } m_model = new ImageProxyModel({}, QBindable(&m_targetSize), QBindable(&m_usedInConfig), this); m_loading.setBinding(m_model->loading().makeBinding()); Q_EMIT wallpaperModelChanged(); } void ImageBackend::ensureSlideshowModel() { if (m_slideshowModel || m_mode != SlideShow) { return; } m_slideshowModel = new SlideModel(QBindable(&m_targetSize), QBindable(&m_usedInConfig), this); m_slideshowModel->setUncheckedSlides(m_uncheckedSlides); m_loading.setBinding(m_slideshowModel->loading().makeBinding()); m_slideFilterModel = new SlideFilterModel(QBindable(&m_usedInConfig), // QBindable(&m_slideshowMode), // QBindable(&m_slideshowFoldersFirst), // this); // setSourceModel(...) must be done in backgroundsFound() to generate a complete random order connect(this, &ImageBackend::uncheckedSlidesChanged, m_slideFilterModel, &SlideFilterModel::invalidateFilter); connect(m_slideshowModel, &SlideModel::dataChanged, this, &ImageBackend::slotSlideModelDataChanged); if (m_usedInConfig) { // When not used in config, slide paths are set in startSlideshow() m_slideshowModel->setSlidePaths(m_slidePaths); if (m_slideshowModel->loading().value()) { connect(m_slideshowModel, &SlideModel::done, this, &ImageBackend::backgroundsFound); } else { // In case it loads immediately m_slideFilterModel->setSourceModel(m_slideshowModel); } } Q_EMIT slideFilterModelChanged(); } void ImageBackend::saveCurrentWallpaper() { if (!m_ready || m_usedInConfig || m_mode != RenderingMode::SlideShow || m_configMap.isNull() || !m_image.isValid()) { return; } QMetaObject::invokeMethod(this, "writeImageConfig", Qt::QueuedConnection, Q_ARG(QString, m_image.toString())); } QAbstractItemModel *ImageBackend::slideFilterModel() const { Q_ASSERT(m_mode == SlideShow); return m_slideFilterModel; } int ImageBackend::slideTimer() const { return m_delay; } void ImageBackend::setSlideTimer(int time) { if (time == m_delay) { return; } m_delay = time; Q_EMIT slideTimerChanged(); startSlideshow(); } QStringList ImageBackend::slidePaths() const { return m_slidePaths; } void ImageBackend::setSlidePaths(const QStringList &slidePaths) { if (slidePaths == m_slidePaths) { return; } m_slidePaths = slidePaths; m_slidePaths.removeAll(QString()); if (!m_slidePaths.isEmpty()) { // Replace 'preferred://wallpaperlocations' with real paths const auto it = std::remove_if(m_slidePaths.begin(), m_slidePaths.end(), [](const QString &path) { return path == QLatin1String("preferred://wallpaperlocations"); }); if (it != m_slidePaths.end()) { m_slidePaths.erase(it, m_slidePaths.end()); m_slidePaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/"), QStandardPaths::LocateDirectory); } } if (!m_usedInConfig) { startSlideshow(); } else if (m_slideshowModel) { // When used in config, m_slideshowModel can be nullptr when the image wallpaper is being used. m_slideshowModel->setSlidePaths(m_slidePaths); } Q_EMIT slidePathsChanged(); } bool ImageBackend::addSlidePath(const QUrl &url) { Q_ASSERT(m_mode == SlideShow); if (url.isEmpty()) { return false; } QString path = url.toLocalFile(); // If path is a file, use its parent folder. const QFileInfo info(path); if (info.isFile()) { path = info.dir().absolutePath(); } const QStringList results = m_slideshowModel->addDirs({path}); if (results.empty()) { return false; } m_slidePaths.append(results); Q_EMIT slidePathsChanged(); return true; } void ImageBackend::removeSlidePath(const QString &path) { Q_ASSERT(m_mode == SlideShow); /* BUG 461003 check path is in the config*/ m_slideshowModel->removeDir(path); if (m_slidePaths.removeOne(path)) { Q_EMIT slidePathsChanged(); } } void ImageBackend::startSlideshow() { if (!m_ready || m_usedInConfig || m_mode != SlideShow || m_pauseSlideshow) { return; } // populate background list m_timer.stop(); ensureSlideshowModel(); m_slideFilterModel->setSourceModel(nullptr); connect(m_slideshowModel, &SlideModel::done, this, &ImageBackend::backgroundsFound); m_slideshowModel->setSlidePaths(m_slidePaths); // TODO: what would be cool: paint on the wallpaper itself a busy widget and perhaps some text // about loading wallpaper slideshow while the thread runs } void ImageBackend::backgroundsFound() { disconnect(m_slideshowModel, &SlideModel::done, this, nullptr); // setSourceModel must be called after the model is loaded to generate a complete random order Q_ASSERT(!m_slideFilterModel->sourceModel()); m_slideFilterModel->setSourceModel(m_slideshowModel); if (m_slideFilterModel->rowCount() == 0 || m_usedInConfig) { return; } // start slideshow m_slideFilterModel->sort(0); m_currentSlide = m_configMap.isNull() || m_slideshowMode == SortingMode::Random ? -1 : m_slideFilterModel->indexOf(m_configMap->value(QStringLiteral("Image")).toString()) - 1; nextSlide(); } QString ImageBackend::nameFilters() const { // i18n people, this isn't a "word puzzle". there is a specific string format for QFileDialog::setNameFilters return i18n("Media Files") + QLatin1String(" (") + suffixes().join(QLatin1Char(' ')) + QLatin1Char(')'); } QQmlPropertyMap *ImageBackend::configMap() const { return m_configMap.data(); } void ImageBackend::setConfigMap(QQmlPropertyMap *configMap) { if (configMap == m_configMap.data()) { return; } m_configMap = configMap; Q_EMIT configMapChanged(); if (!m_configMap.isNull()) { Q_ASSERT(m_configMap->contains(QStringLiteral("Image"))); } saveCurrentWallpaper(); } QString ImageBackend::addUsersWallpaper(const QUrl &url) { Q_ASSERT(m_mode == SingleImage); ensureWallpaperModel(); // The model is not created by default when used in desktop auto results = m_model->addBackground(url.isLocalFile() ? url.toLocalFile() : url.toString()); if (!m_usedInConfig) { m_model->commitAddition(); m_model->deleteLater(); m_model = nullptr; } if (results.empty()) { return QString(); } Q_EMIT settingsChanged(); return results.at(0); } void ImageBackend::nextSlide() { const int rowCount = m_slideFilterModel->rowCount(); if (!m_ready || m_usedInConfig || rowCount == 0) { return; } int previousSlide = m_currentSlide; QString previousPath; if (previousSlide >= 0) { previousPath = m_slideFilterModel->index(m_currentSlide, 0).data(ImageRoles::PackageNameRole).toString(); } if (m_currentSlide >= rowCount - 1 /* ">" in case the last wallpaper is deleted before */ || m_currentSlide < 0) { m_currentSlide = 0; } else { m_currentSlide += 1; } // We are starting again - avoid having the same random order when we restart the slideshow if (m_slideshowMode == SortingMode::Random && m_currentSlide == 0) { m_slideFilterModel->invalidate(); } QString next = m_slideFilterModel->index(m_currentSlide, 0).data(ImageRoles::PackageNameRole).toString(); // And avoid showing the same picture twice if (previousSlide == rowCount - 1 && previousPath == next && rowCount > 1) { m_currentSlide += 1; next = m_slideFilterModel->index(m_currentSlide, 0).data(ImageRoles::PackageNameRole).toString(); } if (next.isEmpty()) { m_image = QUrl::fromLocalFile(previousPath); } else { m_image = QUrl::fromLocalFile(next); Q_EMIT imageChanged(); } saveCurrentWallpaper(); // Synchronize wallpaper transitions across desktops on all screens and activities // by updating the wallpaper in strict time intervals since UNIX epoch, defined by `m_delay`. QDateTime dtnow = QDateTime::currentDateTimeUtc(); qint64 now = dtnow.toMSecsSinceEpoch(); int interval_ms = m_delay * 1000; int offset_ms = now % interval_ms; int duration_ms = interval_ms - offset_ms; // Avoid overlap of wallpaper transition animations by adding a whole interval if `duration_ms` is too short: // 1. Despite `m_timer` being a `Qt::PreciseTimer`, it can still sometimes trigger a bit too early, // causing `duration_ms` to be only a couple of milliseconds. // 2. The slideshow could have been started right before the end of the current interval, // resulting in the same issue. if (duration_ms < m_timer_duration_min) { duration_ms += interval_ms; } // Always add a tiny offset, as `Qt::PreciseTimer` timers are actually not precise. m_timer.start(duration_ms + m_timer_duration_offset); } void ImageBackend::slotSlideModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) { Q_UNUSED(bottomRight); if (!topLeft.isValid()) { return; } if (roles.contains(ImageRoles::ToggleRole)) { if (topLeft.data(ImageRoles::ToggleRole).toBool()) { m_uncheckedSlides.removeOne(topLeft.data(ImageRoles::PackageNameRole).toString()); } else { m_uncheckedSlides.append(topLeft.data(ImageRoles::PackageNameRole).toString()); } Q_EMIT uncheckedSlidesChanged(); } } QStringList ImageBackend::uncheckedSlides() const { return m_uncheckedSlides; } void ImageBackend::setUncheckedSlides(const QStringList &uncheckedSlides) { if (uncheckedSlides == m_uncheckedSlides) { return; } m_uncheckedSlides = uncheckedSlides; if (m_slideshowModel) { m_slideshowModel->setUncheckedSlides(m_uncheckedSlides); } Q_EMIT uncheckedSlidesChanged(); startSlideshow(); } bool ImageBackend::pauseSlideshow() const { return m_pauseSlideshow; } void ImageBackend::setPauseSlideshow(bool pauseSlideshow) { if (m_pauseSlideshow == pauseSlideshow) { return; } m_pauseSlideshow = pauseSlideshow; Q_EMIT pauseSlideshowChanged(); if (!m_slideFilterModel) { return; } if (pauseSlideshow && m_timer.isActive()) { // Pause timer and store the remaining time m_remainingTime = m_timer.remainingTimeAsDuration(); m_timer.stop(); } else if (!pauseSlideshow && !m_timer.isActive()) { if (m_slideFilterModel->rowCount() > 0) { // Resume from the last point m_timer.start(m_remainingTime.value_or(std::chrono::seconds(m_delay))); m_remainingTime.reset(); } else { // Start a new slideshow startSlideshow(); } } } bool ImageBackend::loading() const { return m_loading; }