Verified Commit 86ef83c6 authored by Fushan Wen's avatar Fushan Wen 💬
Browse files

wallpapers/potd: fix multimonitor support

After dataengine was ported away, it requires to use a self-maintained
engine to properly support multi-screen wallpapers.

3 new classes are added:

- PotdEngine: a class to manage clients, create and delete clients on
demand.
- Client: a class to process wallpaper data
- Backend: a class used in QML side, to relay information from the client

The major part of PotdProviderModel are moved to the 3 classes, so the
model class is now a pure model.

BUG: 454333
FIXED-IN: 5.26
parent 01bfc1e3
Pipeline #192365 passed with stage
in 1 minute and 35 seconds
......@@ -33,31 +33,31 @@ Column {
width: Math.round(Screen.width / 10 + Kirigami.Units.smallSpacing * 2)
height: Math.round(Screen.height / 10 + Kirigami.Units.smallSpacing * 2)
image: PotdProviderModelInstance.image
localUrl: PotdProviderModelInstance.localUrl
infoUrl: PotdProviderModelInstance.infoUrl
title: PotdProviderModelInstance.title
author: PotdProviderModelInstance.author
image: backend.image
localUrl: backend.localUrl
infoUrl: backend.infoUrl
title: backend.title
author: backend.author
thumbnailAvailable: !delegate.isNull
thumbnailLoading: PotdProviderModelInstance.loading
thumbnailLoading: backend.loading
actions: [
Kirigami.Action {
icon.name: "document-save"
enabled: PotdProviderModelInstance.localUrl.length > 0
enabled: backend.localUrl.length > 0
visible: enabled
tooltip: i18nc("@action:inmenu wallpaper preview menu", "Save Image as…")
onTriggered: PotdProviderModelInstance.saveImage()
onTriggered: backend.saveImage()
Accessible.description: i18nc("@info:whatsthis for a button and a menu item", "Save today's picture to local disk")
},
Kirigami.Action {
icon.name: "internet-services"
enabled: PotdProviderModelInstance.infoUrl.toString().length > 0
enabled: backend.infoUrl.toString().length > 0
visible: false
tooltip: i18nc("@action:inmenu wallpaper preview menu, will open the website of the wallpaper", "Open Link in Browser…")
onTriggered: Qt.openUrlExternally(PotdProviderModelInstance.infoUrl)
onTriggered: Qt.openUrlExternally(backend.infoUrl)
Accessible.description: i18nc("@info:whatsthis for a menu item", "Open the website of today's picture in the default browser")
}
......
......@@ -7,6 +7,7 @@
import QtQuick 2.5
import QtQuick.Controls 2.8 as QQC2
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import org.kde.kquickcontrols 2.0 as KQC2
import org.kde.kirigami 2.20 as Kirigami
......@@ -23,12 +24,26 @@ Kirigami.FormLayout {
property alias cfg_Color: colorButton.color
property alias formLayout: root
PotdBackend {
id: backend
identifier: cfg_Provider
arguments: {
if (identifier === "unsplash") {
// Needs to specify category for unsplash provider
return [cfg_Category];
} else if (identifier === "bing") {
// Bing supports 1366/1920/UHD resolutions
return [Screen.width, Screen.height, Screen.devicePixelRatio];
}
return [];
}
}
QQC2.ComboBox {
id: providerComboBox
Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label:listbox", "Provider:")
model: PotdProviderModelInstance
currentIndex: model.currentIndex
model: PotdProviderModel { }
currentIndex: model.indexOf(cfg_Provider)
textRole: "display"
valueRole: "id"
onCurrentValueChanged: {
......@@ -250,7 +265,7 @@ Kirigami.FormLayout {
// HACK: If Kirigami.FormData.label is put inside a TextArea,
// the label will not align with text in the TextArea.
Kirigami.FormData.label: i18nc("@label", "Title:")
visible: wallpaperPreview.visible && PotdProviderModelInstance.title.length > 0
visible: wallpaperPreview.visible && backend.title.length > 0
Kirigami.SelectableLabel {
id: titleLabel
......@@ -259,7 +274,7 @@ Kirigami.FormLayout {
Layout.maximumWidth: wallpaperPreview.implicitWidth * 1.5
font.bold: true
text: PotdProviderModelInstance.title
text: backend.title
Accessible.name: titleLabel.Kirigami.FormData.label
}
}
......@@ -270,7 +285,7 @@ Kirigami.FormLayout {
Row {
Kirigami.FormData.label: i18nc("@label", "Author:")
visible: wallpaperPreview.visible && PotdProviderModelInstance.author.length > 0
visible: wallpaperPreview.visible && backend.author.length > 0
Kirigami.SelectableLabel {
id: authorLabel
......@@ -278,7 +293,7 @@ Kirigami.FormLayout {
Layout.fillWidth: true
Layout.maximumWidth: titleLabel.Layout.maximumWidth
text: PotdProviderModelInstance.author
text: backend.author
Accessible.name: authorLabel.Kirigami.FormData.label
}
}
......@@ -296,26 +311,26 @@ Kirigami.FormLayout {
Kirigami.Action {
icon.name: "document-open-folder"
text: i18nc("@action:button", "Open Containing Folder")
visible: PotdProviderModelInstance.saveStatus === Global.Successful
onTriggered: Qt.openUrlExternally(PotdProviderModelInstance.savedFolder)
visible: backend.saveStatus === Global.Successful
onTriggered: Qt.openUrlExternally(backend.savedFolder)
Accessible.description: i18nc("@info:whatsthis for a button", "Open the destination folder where the wallpaper image was saved.")
}
]
onLinkActivated: Qt.openUrlExternally(PotdProviderModelInstance.savedUrl)
onLinkActivated: Qt.openUrlExternally(backend.savedUrl)
Connections {
target: PotdProviderModelInstance
target: backend
function onSaveStatusChanged() {
switch (PotdProviderModelInstance.saveStatus) {
switch (backend.saveStatus) {
case Global.Successful:
saveMessage.text = PotdProviderModelInstance.saveStatusMessage;
saveMessage.text = backend.saveStatusMessage;
saveMessage.type = Kirigami.MessageType.Positive;
break;
case Global.Failed:
saveMessage.text = PotdProviderModelInstance.saveStatusMessage;
saveMessage.text = backend.saveStatusMessage;
saveMessage.type = Kirigami.MessageType.Error;
break;
default:
......
......@@ -15,6 +15,21 @@ import org.kde.plasma.wallpapers.potd 1.0
Rectangle {
id: root
PotdBackend {
id: backend
identifier: wallpaper.configuration.Provider
arguments: {
if (identifier === "unsplash") {
// Needs to specify category for unsplash provider
return [wallpaper.configuration.Category];
} else if (identifier === "bing") {
// Bing supports 1366/1920/UHD resolutions
return [Screen.width, Screen.height, Screen.devicePixelRatio];
}
return [];
}
}
Rectangle {
id: backgroundColor
anchors.fill: parent
......@@ -26,7 +41,7 @@ Rectangle {
QImageItem {
anchors.fill: parent
image: PotdProviderModelInstance.image
image: backend.image
fillMode: wallpaper.configuration.FillMode
smooth: true
......@@ -35,22 +50,4 @@ Rectangle {
wallpaper.repaintNeeded();
}
}
Component.onCompleted: {
PotdProviderModelInstance.identifier = Qt.binding(() => wallpaper.configuration.Provider);
// Needs to specify category for unsplash provider
PotdProviderModelInstance.arguments = Qt.binding(() => {
const identifier = PotdProviderModelInstance.identifier;
if (identifier === "unsplash") {
// Needs to specify category for unsplash provider
return [wallpaper.configuration.Category];
} else if (identifier === "bing") {
// Bing supports 1366/1920/UHD resolutions
return [Screen.width, Screen.height, Screen.devicePixelRatio];
}
return [];
});
PotdProviderModelInstance.running = true;
}
}
set(potd_engine_SRCS
cachedprovider.cpp
potdbackend.cpp
potdengine.cpp
potdprovidermodel.cpp
potdplugin.cpp
)
......
......@@ -56,15 +56,16 @@ void LoadImageThread::run()
Q_EMIT done(data);
}
SaveImageThread::SaveImageThread(const QString &identifier, const PotdProviderData &data)
SaveImageThread::SaveImageThread(const QString &identifier, const QVariantList &args, const PotdProviderData &data)
: m_identifier(identifier)
, m_args(args)
, m_data(data)
{
}
void SaveImageThread::run()
{
m_data.wallpaperLocalUrl = CachedProvider::identifierToPath(m_identifier);
m_data.wallpaperLocalUrl = CachedProvider::identifierToPath(m_identifier, m_args);
m_data.wallpaperImage.save(m_data.wallpaperLocalUrl, "JPEG");
const QString infoPath = m_data.wallpaperLocalUrl + ".json";
......@@ -86,19 +87,28 @@ void SaveImageThread::run()
Q_EMIT done(m_identifier, m_data);
}
QString CachedProvider::identifierToPath(const QString &identifier)
QString CachedProvider::identifierToPath(const QString &identifier, const QVariantList &args)
{
const QString argString = std::accumulate(args.cbegin(), args.cend(), QString(), [](const QString &s, const QVariant &arg) {
if (arg.canConvert(QMetaType::QString)) {
return s + QStringLiteral(":%1").arg(arg.toString());
}
return s;
});
const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/plasma_engine_potd/");
QDir d;
d.mkpath(dataDir);
return dataDir + identifier;
return QStringLiteral("%1%2%3").arg(dataDir, identifier, argString);
}
CachedProvider::CachedProvider(const QString &identifier, QObject *parent)
CachedProvider::CachedProvider(const QString &identifier, const QVariantList &args, QObject *parent)
: PotdProvider(parent, KPluginMetaData(), QVariantList())
, mIdentifier(identifier)
, m_args(args)
{
LoadImageThread *thread = new LoadImageThread(identifierToPath(mIdentifier));
LoadImageThread *thread = new LoadImageThread(identifierToPath(mIdentifier, m_args));
connect(thread, &LoadImageThread::done, this, &CachedProvider::triggerFinished);
QThreadPool::globalInstance()->start(thread);
}
......@@ -120,9 +130,9 @@ void CachedProvider::triggerFinished(const PotdProviderData &data)
Q_EMIT finished(this);
}
bool CachedProvider::isCached(const QString &identifier, bool ignoreAge)
bool CachedProvider::isCached(const QString &identifier, const QVariantList &args, bool ignoreAge)
{
const QString path = identifierToPath(identifier);
const QString path = identifierToPath(identifier, args);
if (!QFile::exists(path)) {
return false;
}
......
......@@ -23,9 +23,10 @@ public:
* Creates a new cached provider.
*
* @param identifier The identifier of the cached picture.
* @param args The arguments of the identifier.
* @param parent The parent object.
*/
CachedProvider(const QString &identifier, QObject *parent);
CachedProvider(const QString &identifier, const QVariantList &args, QObject *parent);
/**
* Returns the identifier of the picture request (name + date).
......@@ -33,20 +34,21 @@ public:
QString identifier() const override;
/**
* Returns whether a picture with the given @p identifier is cached.
* Returns whether a picture with the given @p identifier and @p args is cached.
*/
static bool isCached(const QString &identifier, bool ignoreAge = false);
static bool isCached(const QString &identifier, const QVariantList &args, bool ignoreAge = false);
/**
* Returns a path for the given identifier
*/
static QString identifierToPath(const QString &identifier);
static QString identifierToPath(const QString &identifier, const QVariantList &args);
private Q_SLOTS:
void triggerFinished(const PotdProviderData &data);
private:
QString mIdentifier;
QVariantList m_args;
};
class LoadImageThread : public QObject, public QRunnable
......@@ -69,7 +71,7 @@ class SaveImageThread : public QObject, public QRunnable
Q_OBJECT
public:
SaveImageThread(const QString &identifier, const PotdProviderData &data);
SaveImageThread(const QString &identifier, const QVariantList &args, const PotdProviderData &data);
void run() override;
Q_SIGNALS:
......@@ -77,5 +79,6 @@ Q_SIGNALS:
private:
QString m_identifier;
QVariantList m_args;
PotdProviderData m_data;
};
/*
SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "potdbackend.h"
#include <QDBusConnection>
#include <QFileDialog>
#include <QStandardPaths> // For "Pictures" folder
#include <KIO/CopyJob> // For "Save Image"
#include <KLocalizedString>
#include "potdengine.h"
namespace
{
static PotdEngine *s_engine = nullptr;
static int s_instanceCount = 0;
}
PotdBackend::PotdBackend(QObject *parent)
: QObject(parent)
{
if (!s_engine) {
Q_ASSERT(s_instanceCount == 0);
s_engine = new PotdEngine();
}
s_instanceCount++;
}
PotdBackend::~PotdBackend()
{
s_engine->unregisterClient(m_identifier, m_args);
s_instanceCount--;
if (!s_instanceCount) {
delete s_engine;
s_engine = nullptr;
}
}
void PotdBackend::classBegin()
{
}
void PotdBackend::componentComplete()
{
// don't bother loading single image until all properties have settled
m_ready = true;
// Register the identifier in the data engine
registerClient();
}
QString PotdBackend::identifier() const
{
return m_identifier;
}
void PotdBackend::setIdentifier(const QString &identifier)
{
if (m_identifier == identifier) {
return;
}
if (m_ready) {
s_engine->unregisterClient(m_identifier, m_args);
}
m_identifier = identifier;
registerClient();
Q_EMIT identifierChanged();
}
QVariantList PotdBackend::arguments() const
{
return m_args;
}
void PotdBackend::setArguments(const QVariantList &args)
{
if (m_args == args) {
return;
}
if (m_ready) {
s_engine->unregisterClient(m_identifier, m_args);
}
m_args = args;
registerClient();
Q_EMIT argumentsChanged();
}
QImage PotdBackend::image() const
{
if (!m_client) {
return {};
}
return m_client->m_data.wallpaperImage;
}
bool PotdBackend::loading() const
{
if (!m_client) {
return false;
}
return m_client->m_loading;
}
QString PotdBackend::localUrl() const
{
if (!m_client) {
return {};
}
return m_client->m_data.wallpaperLocalUrl;
}
QUrl PotdBackend::infoUrl() const
{
if (!m_client) {
return {};
}
return m_client->m_data.wallpaperInfoUrl;
}
QUrl PotdBackend::remoteUrl() const
{
if (!m_client) {
return {};
}
return m_client->m_data.wallpaperRemoteUrl;
}
QString PotdBackend::title() const
{
if (!m_client) {
return {};
}
return m_client->m_data.wallpaperTitle;
}
QString PotdBackend::author() const
{
if (!m_client) {
return {};
}
return m_client->m_data.wallpaperAuthor;
}
void PotdBackend::saveImage()
{
if (m_client->m_data.wallpaperLocalUrl.isEmpty()) {
return;
}
auto sanitizeFileName = [](const QString &name) {
if (name.isEmpty()) {
return name;
}
const char notAllowedChars[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`'/\\";
QString sanitizedName(name);
for (const char *c = notAllowedChars; *c; c++) {
sanitizedName.replace(QLatin1Char(*c), QLatin1Char('-'));
}
return sanitizedName;
};
const QStringList &locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
const QString path = locations.isEmpty() ? QStandardPaths::standardLocations(QStandardPaths::HomeLocation).at(0) : locations.at(0);
QString defaultFileName = m_client->m_metadata.name().trimmed();
if (!m_client->m_data.wallpaperTitle.isEmpty()) {
defaultFileName += QLatin1Char('-') + m_client->m_data.wallpaperTitle.trimmed();
if (!m_client->m_data.wallpaperAuthor.isEmpty()) {
defaultFileName += QLatin1Char('-') + m_client->m_data.wallpaperAuthor.trimmed();
}
} else {
// Use current date
if (!defaultFileName.isEmpty()) {
defaultFileName += QLatin1Char('-');
}
defaultFileName += QDate::currentDate().toString();
}
m_savedUrl = QUrl::fromLocalFile( //
QFileDialog::getSaveFileName( //
nullptr, //
i18nc("@title:window", "Save Today's Picture"), //
path + "/" + sanitizeFileName(defaultFileName) + ".jpg", //
i18nc("@label:listbox Template for file dialog", "JPEG image (*.jpeg *.jpg *.jpe)"), //
nullptr, //
QFileDialog::DontConfirmOverwrite // KIO::CopyJob will show the confirmation dialog.
) //
);
if (m_savedUrl.isEmpty() || !m_savedUrl.isValid()) {
return;
}
m_savedFolder = QUrl::fromLocalFile(QFileInfo(m_savedUrl.toLocalFile()).absolutePath());
KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(m_client->m_data.wallpaperLocalUrl), m_savedUrl, KIO::HideProgressInfo);
connect(copyJob, &KJob::finished, this, [this](KJob *job) {
if (job->error()) {
m_saveStatusMessage = job->errorText();
if (m_saveStatusMessage.isEmpty()) {
m_saveStatusMessage = i18nc("@info:status after a save action", "The image was not saved.");
}
m_saveStatus = FileOperationStatus::Failed;
Q_EMIT saveStatusChanged();
} else {
m_saveStatusMessage = i18nc("@info:status after a save action %1 file path %2 basename",
"The image was saved as <a href=\"%1\">%2</a>",
m_savedUrl.toString(),
m_savedUrl.fileName());
m_saveStatus = FileOperationStatus::Successful;
Q_EMIT saveStatusChanged();
}
});
copyJob->start();
}
void PotdBackend::registerClient()
{
if (!m_ready) {
return;
}
m_client = s_engine->registerClient(m_identifier, m_args);
if (!m_client) {
// Invalid identifier
return;
}
connect(m_client, &PotdClient::imageChanged, this, &PotdBackend::imageChanged);
connect(m_client, &PotdClient::loadingChanged, this, &PotdBackend::loadingChanged);
connect(m_client, &PotdClient::localUrlChanged, this, &PotdBackend::localUrlChanged);
connect(m_client, &PotdClient::infoUrlChanged, this, &PotdBackend::infoUrlChanged);
connect(m_client, &PotdClient::remoteUrlChanged, this, &PotdBackend::remoteUrlChanged);
connect(m_client, &PotdClient::titleChanged, this, &PotdBackend::titleChanged);
connect(m_client, &PotdClient::authorChanged, this, &PotdBackend::authorChanged);
// Refresh the desktop wallpaper and the information in config dialog
Q_EMIT imageChanged();
Q_EMIT loadingChanged();
Q_EMIT localUrlChanged();
Q_EMIT infoUrlChanged();
Q_EMIT remoteUrlChanged();
Q_EMIT titleChanged();
Q_EMIT authorChanged();
}
/*
SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later