QortalOS Brooklyn for Raspberry Pi 4
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

472 lines
16 KiB

/*
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <[email protected]>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "job_p.h"
#include "debug.h"
#include <QDBusConnection>
#include <QDebug>
#include <QTimer>
#include <KFilePlacesModel>
#include <KLocalizedString>
#include <KShell>
#include <kio/global.h>
#include "jobviewv2adaptor.h"
#include "jobviewv3adaptor.h"
using namespace NotificationManager;
JobPrivate::JobPrivate(uint id, QObject *parent)
: QObject(parent)
, m_id(id)
{
m_showTimer.setSingleShot(true);
connect(&m_showTimer, &QTimer::timeout, this, &JobPrivate::requestShow);
m_objectPath.setPath(QStringLiteral("/org/kde/notificationmanager/jobs/JobView_%1").arg(id));
// TODO also v1? it's identical to V2 except it doesn't have setError method so supporting it should be easy
new JobViewV2Adaptor(this);
new JobViewV3Adaptor(this);
QDBusConnection::sessionBus().registerObject(m_objectPath.path(), this);
}
JobPrivate::~JobPrivate() = default;
void JobPrivate::requestShow()
{
if (!m_showRequested) {
m_showRequested = true;
Q_EMIT showRequested();
}
}
QDBusObjectPath JobPrivate::objectPath() const
{
return m_objectPath;
}
QSharedPointer<KFilePlacesModel> JobPrivate::createPlacesModel()
{
static QWeakPointer<KFilePlacesModel> s_instance;
if (!s_instance) {
QSharedPointer<KFilePlacesModel> ptr(new KFilePlacesModel());
s_instance = ptr.toWeakRef();
return ptr;
}
return s_instance.toStrongRef();
}
QUrl JobPrivate::localFileOrUrl(const QString &urlString)
{
QUrl url(urlString);
if (url.scheme().isEmpty()) {
url = QUrl::fromLocalFile(urlString);
}
return url;
}
QUrl JobPrivate::destUrl() const
{
QUrl url = m_destUrl;
// In case of a single file and no destUrl, try using the second label (most likely "Destination")...
if (!url.isValid() && m_totalFiles == 1) {
url = localFileOrUrl(m_descriptionValue2).adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
}
return url;
}
QString JobPrivate::prettyUrl(const QUrl &_url) const
{
QUrl url(_url);
if (!url.isValid()) {
return QString();
}
if (url.path().endsWith(QLatin1String("/."))) {
url.setPath(url.path().chopped(2));
}
if (!m_placesModel) {
m_placesModel = createPlacesModel();
}
// Mimic KUrlNavigator and show a pretty place name,
// for example Documents/foo/bar rather than /home/user/Documents/foo/bar
const QModelIndex closestIdx = m_placesModel->closestItem(url);
if (closestIdx.isValid()) {
const QUrl placeUrl = m_placesModel->url(closestIdx);
QString text = m_placesModel->text(closestIdx);
QString pathInsidePlace = url.path().mid(placeUrl.path().length());
if (!pathInsidePlace.isEmpty() && !pathInsidePlace.startsWith(QLatin1Char('/'))) {
pathInsidePlace.prepend(QLatin1Char('/'));
}
if (pathInsidePlace != QLatin1Char('/')) {
text.append(pathInsidePlace);
}
return text;
}
if (url.isLocalFile()) {
return KShell::tildeCollapse(url.toLocalFile());
}
return url.toDisplayString(QUrl::RemoveUserInfo);
}
void JobPrivate::updateHasDetails()
{
// clang-format off
const bool hasDetails = m_totalBytes > 0
|| m_totalFiles > 0
|| m_totalDirectories > 0
|| m_totalItems > 0
|| m_processedBytes > 0
|| m_processedFiles > 0
|| m_processedDirectories > 0
|| m_processedItems > 0
|| !m_descriptionValue1.isEmpty()
|| !m_descriptionValue2.isEmpty()
|| m_speed > 0;
// clang-format on
if (m_hasDetails != hasDetails) {
m_hasDetails = hasDetails;
Q_EMIT static_cast<Job *>(parent())->hasDetailsChanged();
}
}
QString JobPrivate::text() const
{
if (!m_errorText.isEmpty()) {
return m_errorText;
}
if (!m_infoMessage.isEmpty()) {
return m_infoMessage;
}
const QUrl destUrl = this->destUrl();
const QString prettyDestUrl = prettyUrl(destUrl);
QString destUrlString;
if (!prettyDestUrl.isEmpty()) {
// Turn destination into a clickable hyperlink
destUrlString = QStringLiteral("<a href=\"%1\">%2</a>").arg(destUrl.toString(QUrl::PrettyDecoded), prettyDestUrl.toHtmlEscaped());
}
if (m_totalFiles == 0) {
if (!destUrlString.isEmpty()) {
if (m_processedFiles > 0) {
return i18ncp("Copying n files to location", "%1 file to %2", "%1 files to %2", m_processedFiles, destUrlString);
}
return i18nc("Copying unknown amount of files to location", "to %1", destUrlString);
} else if (m_processedFiles > 0) {
return i18ncp("Copying n files", "%1 file", "%1 files", m_processedFiles);
}
} else if (m_totalFiles == 1) {
const QString currentFileName = descriptionUrl().fileName().toHtmlEscaped();
if (!destUrlString.isEmpty()) {
if (!currentFileName.isEmpty()) {
return i18nc("Copying file to location", "%1 to %2", currentFileName, destUrlString);
} else {
return i18ncp("Copying n files to location", "%1 file to %2", "%1 files to %2", m_totalFiles, destUrlString);
}
} else if (!currentFileName.isEmpty()) {
return currentFileName;
} else {
return i18ncp("Copying n files", "%1 file", "%1 files", m_totalFiles);
}
} else if (m_totalFiles > 1) {
if (!destUrlString.isEmpty()) {
if (m_processedFiles > 0 && m_processedFiles <= m_totalFiles) {
return i18ncp("Copying n of m files to locaton", "%2 of %1 file to %3", "%2 of %1 files to %3", m_totalFiles, m_processedFiles, destUrlString);
}
return i18ncp("Copying n files to location",
"%1 file to %2",
"%1 files to %2",
m_processedFiles > 0 ? m_processedFiles : m_totalFiles,
destUrlString);
}
if (m_processedFiles > 0 && m_processedFiles <= m_totalFiles) {
return i18ncp("Copying n of m files", "%2 of %1 file", "%2 of %1 files", m_totalFiles, m_processedFiles);
}
return i18ncp("Copying n files", "%1 file", "%1 files", m_processedFiles > 0 ? m_processedFiles : m_totalFiles);
}
qCInfo(NOTIFICATIONMANAGER) << "Failed to generate job text for job with following properties:";
qCInfo(NOTIFICATIONMANAGER).nospace() << " processedFiles = " << m_processedFiles << ", totalFiles = " << m_totalFiles;
qCInfo(NOTIFICATIONMANAGER).nospace() << " current file name = " << descriptionUrl().fileName();
qCInfo(NOTIFICATIONMANAGER).nospace() << " destination url = " << destUrl;
qCInfo(NOTIFICATIONMANAGER).nospace() << " label1 = " << m_descriptionLabel1 << ", value1 = " << m_descriptionValue1;
qCInfo(NOTIFICATIONMANAGER).nospace() << " label2 = " << m_descriptionLabel2 << ", value2 = " << m_descriptionValue2;
return QString();
}
void JobPrivate::delayedShow(std::chrono::milliseconds delay, ShowConditions showConditions)
{
m_showConditions = showConditions;
if (showConditions.testFlag(ShowCondition::OnTimeout)) {
m_showTimer.start(delay);
}
}
void JobPrivate::kill()
{
Q_EMIT cancelRequested();
// In case the application doesn't respond, remove the job
if (!m_killTimer) {
m_killTimer = new QTimer(this);
m_killTimer->setSingleShot(true);
connect(m_killTimer, &QTimer::timeout, this, [this] {
qCWarning(NOTIFICATIONMANAGER) << "Application" << m_applicationName << "failed to respond to a cancel request in time";
Job *job = static_cast<Job *>(parent());
job->setError(KIO::ERR_USER_CANCELED);
job->setState(Notifications::JobStateStopped);
finish();
});
}
if (!m_killTimer->isActive()) {
m_killTimer->start(2000);
}
}
QUrl JobPrivate::descriptionUrl() const
{
QUrl url = localFileOrUrl(m_descriptionValue2);
if (!url.isValid()) {
url = localFileOrUrl(m_descriptionValue1);
}
return url;
}
void JobPrivate::finish()
{
// Unregister the dbus service since the client is done with it
QDBusConnection::sessionBus().unregisterObject(m_objectPath.path());
// When user canceled job or a transient job finished successfully, remove it without notice
if (m_error == KIO::ERR_USER_CANCELED || (!m_error && m_transient)) {
Q_EMIT closed();
return;
}
if (m_killTimer) {
m_killTimer->stop();
}
Job *job = static_cast<Job *>(parent());
// update timestamp
job->resetUpdated();
// when it was hidden in history, bring it up again
job->setDismissed(false);
}
// JobViewV2
void JobPrivate::terminate(const QString &errorMessage)
{
Job *job = static_cast<Job *>(parent());
// forward to JobViewV3. In V2 we get a setError before a terminate
// so we want to forward the current error to the V3 call.
terminate(job->error(), errorMessage, {});
}
void JobPrivate::setSuspended(bool suspended)
{
Job *job = static_cast<Job *>(parent());
if (suspended) {
job->setState(Notifications::JobStateSuspended);
} else {
job->setState(Notifications::JobStateRunning);
}
}
void JobPrivate::setTotalAmount(quint64 amount, const QString &unit)
{
if (unit == QLatin1String("bytes")) {
updateField(amount, m_totalBytes, &Job::totalBytesChanged);
} else if (unit == QLatin1String("files")) {
updateField(amount, m_totalFiles, &Job::totalFilesChanged);
} else if (unit == QLatin1String("dirs")) {
updateField(amount, m_totalDirectories, &Job::totalDirectoriesChanged);
} else if (unit == QLatin1String("items")) {
updateField(amount, m_totalItems, &Job::totalItemsChanged);
}
updateHasDetails();
}
void JobPrivate::setProcessedAmount(quint64 amount, const QString &unit)
{
if (unit == QLatin1String("bytes")) {
updateField(amount, m_processedBytes, &Job::processedBytesChanged);
} else if (unit == QLatin1String("files")) {
updateField(amount, m_processedFiles, &Job::processedFilesChanged);
} else if (unit == QLatin1String("dirs")) {
updateField(amount, m_processedDirectories, &Job::processedDirectoriesChanged);
} else if (unit == QLatin1String("items")) {
updateField(amount, m_processedItems, &Job::processedItemsChanged);
}
updateHasDetails();
}
void JobPrivate::setPercent(uint percent)
{
const int percentage = static_cast<int>(percent);
if (m_percentage != percentage) {
m_percentage = percentage;
Q_EMIT static_cast<Job *>(parent())->percentageChanged(percentage);
}
}
void JobPrivate::setSpeed(quint64 bytesPerSecond)
{
updateField(bytesPerSecond, m_speed, &Job::speedChanged);
updateHasDetails();
}
// NOTE infoMessage isn't supposed to be the "Copying..." heading but e.g. a "Connecting to server..." status message
// JobViewV1/V2 got that wrong but JobView3 uses "title" and "infoMessage" correctly respectively.
void JobPrivate::setInfoMessage(const QString &infoMessage)
{
updateField(infoMessage, m_summary, &Job::summaryChanged);
}
bool JobPrivate::setDescriptionField(uint number, const QString &name, const QString &value)
{
bool dirty = false;
if (number == 0) {
dirty |= updateField(name, m_descriptionLabel1, &Job::descriptionLabel1Changed);
dirty |= updateField(value, m_descriptionValue1, &Job::descriptionValue1Changed);
} else if (number == 1) {
dirty |= updateField(name, m_descriptionLabel2, &Job::descriptionLabel2Changed);
dirty |= updateField(value, m_descriptionValue2, &Job::descriptionValue2Changed);
}
if (dirty) {
Q_EMIT static_cast<Job *>(parent())->descriptionUrlChanged();
updateHasDetails();
}
return false;
}
void JobPrivate::clearDescriptionField(uint number)
{
setDescriptionField(number, QString(), QString());
}
void JobPrivate::setDestUrl(const QDBusVariant &urlVariant)
{
QUrl destUrl = QUrl(urlVariant.variant().toUrl().adjusted(QUrl::StripTrailingSlash)); // urgh
if (destUrl.scheme().isEmpty()) {
qCInfo(NOTIFICATIONMANAGER) << "Job from" << m_applicationName << "set a destUrl" << destUrl
<< "without a scheme (assuming 'file'), this is an application bug!";
destUrl.setScheme(QStringLiteral("file"));
}
updateField(destUrl, m_destUrl, &Job::destUrlChanged);
}
void JobPrivate::setError(uint errorCode)
{
static_cast<Job *>(parent())->setError(errorCode);
}
// JobViewV3
void JobPrivate::terminate(uint errorCode, const QString &errorMessage, const QVariantMap &hints)
{
Q_UNUSED(hints) // reserved for future extension
Job *job = static_cast<Job *>(parent());
job->setError(errorCode);
job->setErrorText(errorMessage);
// Request show just before changing state to stopped, so we're not discarded
if (m_showConditions.testFlag(ShowCondition::OnTermination)) {
requestShow();
}
job->setState(Notifications::JobStateStopped);
finish();
}
void JobPrivate::update(const QVariantMap &properties)
{
auto end = properties.end();
auto it = properties.find(QStringLiteral("title"));
if (it != end) {
updateField(it->toString(), m_summary, &Job::summaryChanged);
}
it = properties.find(QStringLiteral("infoMessage"));
if (it != end) {
// InfoMessage is exposed via text()/BodyRole, not via public API, hence no public signal
const QString infoMessage = it->toString();
if (m_infoMessage != infoMessage) {
m_infoMessage = it->toString();
Q_EMIT infoMessageChanged();
}
}
it = properties.find(QStringLiteral("percent"));
if (it != end) {
setPercent(it->toUInt());
}
it = properties.find(QStringLiteral("destUrl"));
if (it != end) {
const QUrl destUrl = QUrl(it->toUrl().adjusted(QUrl::StripTrailingSlash)); // urgh
updateField(destUrl, m_destUrl, &Job::destUrlChanged);
}
it = properties.find(QStringLiteral("speed"));
if (it != end) {
setSpeed(it->value<qulonglong>());
}
updateFieldFromProperties(properties, QStringLiteral("processedFiles"), m_processedFiles, &Job::processedFilesChanged);
updateFieldFromProperties(properties, QStringLiteral("processedBytes"), m_processedBytes, &Job::processedBytesChanged);
updateFieldFromProperties(properties, QStringLiteral("processedDirectories"), m_processedDirectories, &Job::processedDirectoriesChanged);
updateFieldFromProperties(properties, QStringLiteral("processedItems"), m_processedItems, &Job::processedItemsChanged);
updateFieldFromProperties(properties, QStringLiteral("totalFiles"), m_totalFiles, &Job::totalFilesChanged);
updateFieldFromProperties(properties, QStringLiteral("totalBytes"), m_totalBytes, &Job::totalBytesChanged);
updateFieldFromProperties(properties, QStringLiteral("totalDirectories"), m_totalDirectories, &Job::totalDirectoriesChanged);
updateFieldFromProperties(properties, QStringLiteral("totalItems"), m_totalItems, &Job::totalItemsChanged);
updateFieldFromProperties(properties, QStringLiteral("descriptionLabel1"), m_descriptionLabel1, &Job::descriptionLabel1Changed);
updateFieldFromProperties(properties, QStringLiteral("descriptionValue1"), m_descriptionValue1, &Job::descriptionValue1Changed);
updateFieldFromProperties(properties, QStringLiteral("descriptionLabel2"), m_descriptionLabel2, &Job::descriptionLabel2Changed);
updateFieldFromProperties(properties, QStringLiteral("descriptionValue2"), m_descriptionValue2, &Job::descriptionValue2Changed);
it = properties.find(QStringLiteral("suspended"));
if (it != end) {
setSuspended(it->toBool());
}
updateHasDetails();
if (!m_summary.isEmpty() && m_showConditions.testFlag(ShowCondition::OnSummary)) {
requestShow();
}
}