/* 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 #include #include #include #include #include "debug.h" #include "finder/packagefinder.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_slideFilterModel(new SlideFilterModel(this)) , m_isDarkColorScheme(isDarkColorScheme()) { connect(&m_timer, &QTimer::timeout, this, &ImageBackend::nextSlide); useSingleImageDefaults(); } ImageBackend::~ImageBackend() { delete m_dialog; } void ImageBackend::classBegin() { } void ImageBackend::componentComplete() { // don't bother loading single image until all properties have settled // otherwise we would load a too small image (initial view size) just // to load the proper one afterwards etc etc m_ready = true; // Follow system color scheme connect(qGuiApp, &QGuiApplication::paletteChanged, this, &ImageBackend::slotSystemPaletteChanged); if (m_mode == SingleImage) { setSingleImage(); } else if (m_mode == SlideShow) { startSlideshow(); } } QString ImageBackend::image() const { return m_image.toString(); } void ImageBackend::setImage(const QString &url) { if (m_image.toString() == url || url.isEmpty()) { return; } m_image = QUrl(url); Q_EMIT imageChanged(); setSingleImage(); } QUrl ImageBackend::modelImage() const { return m_modelImage; } ImageBackend::RenderingMode ImageBackend::renderingMode() const { return m_mode; } void ImageBackend::setRenderingMode(RenderingMode mode) { if (mode == m_mode) { return; } m_mode = mode; if (m_mode == SlideShow) { startSlideshow(); } else { // we need to reset the preferred image setSingleImage(); } } SortingMode::Mode ImageBackend::slideshowMode() const { return m_slideshowMode; } void ImageBackend::setSlideshowMode(SortingMode::Mode slideshowMode) { if (slideshowMode == m_slideshowMode) { return; } m_slideshowMode = slideshowMode; m_slideFilterModel->setSortingMode(m_slideshowMode, m_slideshowFoldersFirst); m_slideFilterModel->sort(0); if (m_mode == SlideShow) { startSlideshow(); } Q_EMIT slideshowModeChanged(); } bool ImageBackend::slideshowFoldersFirst() const { return m_slideshowFoldersFirst; } void ImageBackend::setSlideshowFoldersFirst(bool slideshowFoldersFirst) { if (slideshowFoldersFirst == m_slideshowFoldersFirst) { return; } m_slideshowFoldersFirst = slideshowFoldersFirst; m_slideFilterModel->setSortingMode(m_slideshowMode, m_slideshowFoldersFirst); m_slideFilterModel->sort(0); if (m_mode == SlideShow) { startSlideshow(); } Q_EMIT slideshowFoldersFirstChanged(); } QSize ImageBackend::targetSize() const { return m_targetSize; } void ImageBackend::setTargetSize(const QSize &size) { if (m_targetSize == size) { return; } m_targetSize = size; if (m_ready && m_providerType == Provider::Package) { Q_EMIT modelImageChanged(); } // Will relay to ImageProxyModel Q_EMIT targetSizeChanged(m_targetSize); } void ImageBackend::useSingleImageDefaults() { m_image.clear(); // Try from the look and feel package first, then from the plasma theme KPackage::Package lookAndFeelPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "KDE"); const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); // If empty, it will be the default (currently Breeze) if (!packageName.isEmpty()) { lookAndFeelPackage.setPath(packageName); } KConfigGroup lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(lookAndFeelPackage.filePath("defaults")), "Wallpaper"); const QString image = lnfDefaultsConfig.readEntry("Image", ""); KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); if (!image.isEmpty()) { package.setPath(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/") + image, QStandardPaths::LocateDirectory)); if (package.isValid()) { m_image = QUrl::fromLocalFile(package.path()); } } // Try to get a default from the plasma theme if (m_image.isEmpty()) { Plasma::Theme theme; QString path = theme.wallpaperPath(); int index = path.indexOf(QLatin1String("/contents/images/")); if (index > -1) { // We have file from package -> get path to package m_image = QUrl::fromLocalFile(path.left(index)); } else { m_image = QUrl::fromLocalFile(path); } package.setPath(m_image.toLocalFile()); if (!package.isValid()) { return; } } PackageFinder::findPreferredImageInPackage(package, m_targetSize); // Make sure the image can be read, or there will be dead loops. if (m_image.isEmpty() || QImage(package.filePath("preferred")).isNull()) { return; } Q_EMIT imageChanged(); setSingleImage(); } QAbstractItemModel *ImageBackend::wallpaperModel() { if (!m_model) { m_model = new ImageProxyModel({}, m_targetSize, this); connect(this, &ImageBackend::targetSizeChanged, m_model, &ImageProxyModel::targetSizeChanged); connect(m_model, &ImageProxyModel::loadingChanged, this, &ImageBackend::loadingChanged); } return m_model; } SlideModel *ImageBackend::slideshowModel() { if (!m_slideshowModel) { m_slideshowModel = new SlideModel(m_targetSize, this); m_slideshowModel->setUncheckedSlides(m_uncheckedSlides); connect(this, &ImageBackend::uncheckedSlidesChanged, m_slideFilterModel, &SlideFilterModel::invalidateFilter); connect(this, &ImageBackend::targetSizeChanged, m_slideshowModel, &SlideModel::targetSizeChanged); connect(m_slideshowModel, &SlideModel::dataChanged, this, &ImageBackend::slotSlideModelDataChanged); connect(m_slideshowModel, &SlideModel::loadingChanged, this, &ImageBackend::loadingChanged); } return m_slideshowModel; } bool ImageBackend::isDarkColorScheme(const QPalette &palette) const noexcept { // 192 is from kcm_colors if (palette == QPalette()) { return qGray(qGuiApp->palette().window().color().rgb()) < 192; } return qGray(palette.window().color().rgb()) < 192; } QAbstractItemModel *ImageBackend::slideFilterModel() { if (!m_slideFilterModel->sourceModel()) { // make sure it's created connect(slideshowModel(), &SlideModel::done, this, &ImageBackend::backgroundsFound); } return m_slideFilterModel; } int ImageBackend::slideTimer() const { return m_delay; } void ImageBackend::setSlideTimer(int time) { if (time == m_delay || m_mode != SlideShow) { return; } m_delay = time; startSlideshow(); Q_EMIT slideTimerChanged(); } 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 { slideshowModel()->setSlidePaths(m_slidePaths); } Q_EMIT slidePathsChanged(); } void ImageBackend::showAddSlidePathsDialog() { QFileDialog *dialog = new QFileDialog(nullptr, i18n("Directory with the wallpaper to show slides from"), QString()); dialog->setAttribute(Qt::WA_DeleteOnClose, true); dialog->setOptions(QFileDialog::ShowDirsOnly); dialog->setAcceptMode(QFileDialog::AcceptOpen); connect(dialog, &QDialog::accepted, this, &ImageBackend::addDirFromSelectionDialog); dialog->show(); } void ImageBackend::addSlidePath(const QUrl &url) { if (url.isEmpty()) { return; } 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; } m_slidePaths.append(results); Q_EMIT slidePathsChanged(); } void ImageBackend::removeSlidePath(const QString &path) { if (m_mode != SlideShow) { return; } const QString result = m_slideshowModel->removeDir(path); if (result.isEmpty()) { return; } if (m_slidePaths.removeOne(path)) { Q_EMIT slidePathsChanged(); } } void ImageBackend::addDirFromSelectionDialog() { QFileDialog *dialog = qobject_cast(sender()); if (dialog) { addSlidePath(dialog->directoryUrl()); } } void ImageBackend::setSingleImage() { if (!m_ready || m_image.isEmpty()) { return; } // supposedly QSize::isEmpty() is true if "either width or height are >= 0" if (!m_targetSize.width() || !m_targetSize.height()) { return; } if (m_image.isLocalFile()) { const QFileInfo info(m_image.toLocalFile()); if (!info.exists()) { return; } if (info.isFile()) { m_providerType = Provider::Image; } else { m_providerType = Provider::Package; } } else { // The url can be without file://, try again. const QFileInfo info(m_image.toString()); if (!info.exists()) { return; } if (info.isFile()) { m_providerType = Provider::Image; } else { m_providerType = Provider::Package; } m_image = QUrl::fromLocalFile(info.filePath()); } switch (m_providerType) { case Provider::Image: m_modelImage = m_image; break; case Provider::Package: { // Use a custom image provider QUrl url(QStringLiteral("image://package/get")); QUrlQuery urlQuery(url); urlQuery.addQueryItem(QStringLiteral("dir"), m_image.toLocalFile()); url.setQuery(urlQuery); m_modelImage = url; break; } } if (!m_modelImage.isEmpty()) { Q_EMIT modelImageChanged(); } } void ImageBackend::startSlideshow() { if (!m_ready || m_usedInConfig || m_mode != SlideShow) { return; } // populate background list m_timer.stop(); slideshowModel()->setSlidePaths(m_slidePaths); connect(m_slideshowModel, &SlideModel::done, this, &ImageBackend::backgroundsFound); // 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 m_slideFilterModel->setSourceModel(m_slideshowModel); m_slideFilterModel->invalidate(); if (m_slideFilterModel->rowCount() == 0 || m_usedInConfig) { return; } // start slideshow if (m_currentSlide == -1) { m_currentSlide = m_slideFilterModel->indexOf(m_image.toString()) - 1; } else { m_currentSlide = -1; } m_slideFilterModel->sort(0); nextSlide(); } void ImageBackend::slotSystemPaletteChanged(const QPalette &palette) { if (m_providerType != Provider::Package || m_usedInConfig) { // Currently only KPackage supports adaptive wallpapers return; } const bool dark = isDarkColorScheme(palette); if (dark == m_isDarkColorScheme) { return; } m_isDarkColorScheme = dark; Q_EMIT colorSchemeChanged(); } void ImageBackend::showFileDialog() { if (!m_dialog) { QString path; const QStringList &locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); if (!locations.isEmpty()) { path = locations.at(0); } else { // HomeLocation is guaranteed not to be empty. path = QStandardPaths::standardLocations(QStandardPaths::HomeLocation).at(0); } QMimeDatabase db; QStringList imageGlobPatterns; const auto supportedMimeTypes = QImageReader::supportedMimeTypes(); for (const QByteArray &mimeType : supportedMimeTypes) { QMimeType mime(db.mimeTypeForName(QString::fromLatin1(mimeType))); imageGlobPatterns << mime.globPatterns(); } m_dialog = new QFileDialog(nullptr, i18n("Open Image"), path, i18n("Image Files") + " (" + imageGlobPatterns.join(' ') + ')'); // i18n people, this isn't a "word puzzle". there is a specific string format for QFileDialog::setNameFilters m_dialog->setFileMode(QFileDialog::ExistingFiles); connect(m_dialog, &QDialog::accepted, this, &ImageBackend::slotWallpaperBrowseCompleted); } m_dialog->show(); m_dialog->raise(); m_dialog->activateWindow(); } void ImageBackend::slotWallpaperBrowseCompleted() { if (!m_model || !m_dialog) { return; } const QStringList selectedFiles = m_dialog->selectedFiles(); if (selectedFiles.empty()) { return; } for (const QString &p : selectedFiles) { m_model->addBackground(p); } Q_EMIT settingsChanged(); } QString ImageBackend::addUsersWallpaper(const QUrl &url) { auto results = static_cast(wallpaperModel())->addBackground(url.toLocalFile()); 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 || 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(); } m_timer.stop(); m_timer.start(m_delay * 1000); if (next.isEmpty()) { m_image = QUrl(previousPath); // setSingleImage will add "file://" } else { m_image = QUrl(next); Q_EMIT imageChanged(); setSingleImage(); } } void ImageBackend::slotSlideModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &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(); } } void ImageBackend::openFolder(const QString &path) { // TODO: Move to SlideFilterModel auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(path)); auto *delegate = new KNotificationJobUiDelegate; delegate->setAutoErrorHandlingEnabled(true); job->setUiDelegate(delegate); job->start(); } void ImageBackend::openModelImage() const { QUrl url; switch (m_providerType) { case Provider::Image: { url = m_image; break; } case Provider::Package: { KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); package.setPath(m_image.toLocalFile()); if (!package.isValid()) { return; } PackageFinder::findPreferredImageInPackage(package, m_targetSize); url = QUrl::fromLocalFile(package.filePath("preferred")); if (isDarkColorScheme()) { const QUrl darkUrl = package.fileUrl("preferredDark"); if (!darkUrl.isEmpty()) { url = darkUrl; } } break; } } KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url); job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); job->start(); } 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::loading() const { if (renderingMode() == SingleImage && m_model) { return m_model->loading(); } else if (renderingMode() == SlideShow && m_slideshowModel) { return m_slideshowModel->loading(); } return false; }