File indexing completed on 2024-04-28 17:06:05

0001 /*
0002     SPDX-FileCopyrightText: 2016-2022 Krusader Krew <https://krusader.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "jobman.h"
0008 
0009 // QtCore
0010 #include <QDebug>
0011 #include <QUrl>
0012 // QtWidgets
0013 #include <QComboBox>
0014 #include <QLabel>
0015 #include <QMenu>
0016 #include <QPushButton>
0017 #include <QVBoxLayout>
0018 #include <QWidgetAction>
0019 
0020 #include <KConfigCore/KSharedConfig>
0021 #include <KI18n/KLocalizedString>
0022 #include <KIOWidgets/KIO/FileUndoManager>
0023 #include <kio_version.h>
0024 
0025 #include "../icon.h"
0026 #include "../krglobal.h"
0027 #include "krjob.h"
0028 
0029 const int MAX_OLD_MENU_ACTIONS = 10;
0030 
0031 /** The menu action entry for a job in the popup menu.*/
0032 class JobMenuAction : public QWidgetAction
0033 {
0034     Q_OBJECT
0035 public:
0036     JobMenuAction(KrJob *krJob, QObject *parent, KJob *kJob = nullptr)
0037         : QWidgetAction(parent)
0038         , m_krJob(krJob)
0039     {
0040         QWidget *container = new QWidget();
0041         auto *layout = new QGridLayout(container);
0042         m_description = new QLabel(krJob->description());
0043         m_progressBar = new QProgressBar();
0044         layout->addWidget(m_description, 0, 0, 1, 3);
0045         layout->addWidget(m_progressBar, 1, 0);
0046 
0047         m_pauseResumeButton = new QPushButton();
0048         updatePauseResumeButton();
0049         connect(m_pauseResumeButton, &QPushButton::clicked, this, &JobMenuAction::slotPauseResumeButtonClicked);
0050         layout->addWidget(m_pauseResumeButton, 1, 1);
0051 
0052         m_cancelButton = new QPushButton();
0053         m_cancelButton->setIcon(Icon("remove"));
0054         m_cancelButton->setToolTip(i18n("Cancel Job"));
0055         connect(m_cancelButton, &QPushButton::clicked, this, &JobMenuAction::slotCancelButtonClicked);
0056         layout->addWidget(m_cancelButton, 1, 2);
0057 
0058         setDefaultWidget(container);
0059 
0060         if (kJob) {
0061             slotStarted(kJob);
0062         } else {
0063             connect(krJob, &KrJob::started, this, &JobMenuAction::slotStarted);
0064         }
0065 
0066         connect(krJob, &KrJob::terminated, this, &JobMenuAction::slotTerminated);
0067     }
0068 
0069     bool isDone()
0070     {
0071         return !m_krJob;
0072     }
0073 
0074 protected slots:
0075     void slotDescription(KJob *, const QString &description, const QPair<QString, QString> &field1, const QPair<QString, QString> &field2)
0076     {
0077         const QPair<QString, QString> textField = !field2.first.isEmpty() ? field2 : field1;
0078         QString text = description;
0079         if (!textField.first.isEmpty()) {
0080             text += QString(" - %1: %2").arg(textField.first, textField.second);
0081         }
0082         m_description->setText(text);
0083 
0084         if (!field2.first.isEmpty() && !field1.first.isEmpty()) {
0085             // NOTE: tooltips for QAction items in menu are not shown
0086             m_progressBar->setToolTip(QString("%1: %2").arg(field1.first, field1.second));
0087         }
0088     }
0089 
0090     void slotPercent(KJob *, unsigned long percent)
0091     {
0092         m_progressBar->setValue(static_cast<int>(percent));
0093     }
0094 
0095     void updatePauseResumeButton()
0096     {
0097         m_pauseResumeButton->setIcon(Icon(m_krJob->isRunning() ? "media-playback-pause" : m_krJob->isPaused() ? "media-playback-start" : "chronometer-start"));
0098         m_pauseResumeButton->setToolTip(m_krJob->isRunning() ? i18n("Pause Job") : m_krJob->isPaused() ? i18n("Resume Job") : i18n("Start Job"));
0099     }
0100 
0101     void slotResult(KJob *job)
0102     {
0103         // NOTE: m_job may already set to NULL now
0104         if (!job->error()) {
0105             // percent signal is not reliable, set manually
0106             m_progressBar->setValue(100);
0107         }
0108     }
0109 
0110     void slotTerminated()
0111     {
0112         qDebug() << "job description=" << m_krJob->description();
0113         m_pauseResumeButton->setEnabled(false);
0114         m_cancelButton->setIcon(Icon("edit-clear"));
0115         m_cancelButton->setToolTip(i18n("Clear"));
0116 
0117         m_krJob = nullptr;
0118     }
0119 
0120     void slotPauseResumeButtonClicked()
0121     {
0122         if (!m_krJob)
0123             return;
0124 
0125         if (m_krJob->isRunning())
0126             m_krJob->pause();
0127         else
0128             m_krJob->start();
0129     }
0130 
0131     void slotCancelButtonClicked()
0132     {
0133         if (m_krJob) {
0134             m_krJob->cancel();
0135         } else {
0136             deleteLater();
0137         }
0138     }
0139 
0140 private slots:
0141     void slotStarted(KJob *job)
0142     {
0143         connect(job, &KJob::description, this, &JobMenuAction::slotDescription);
0144         connect(job, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotPercent(KJob *, ulong)));
0145         connect(job, &KJob::suspended, this, &JobMenuAction::updatePauseResumeButton);
0146         connect(job, &KJob::resumed, this, &JobMenuAction::updatePauseResumeButton);
0147         connect(job, &KJob::result, this, &JobMenuAction::slotResult);
0148         connect(job, &KJob::warning, this, [](KJob *, const QString &plain, const QString &) {
0149             qWarning() << "unexpected job warning: " << plain;
0150         });
0151 
0152         updatePauseResumeButton();
0153     }
0154 
0155 private:
0156     KrJob *m_krJob;
0157 
0158     QLabel *m_description;
0159     QProgressBar *m_progressBar;
0160     QPushButton *m_pauseResumeButton;
0161     QPushButton *m_cancelButton;
0162 };
0163 
0164 #include "jobman.moc" // required for class definitions with Q_OBJECT macro in implementation files
0165 
0166 const QString JobMan::sDefaultToolTip = i18n("No jobs");
0167 
0168 JobMan::JobMan(QObject *parent)
0169     : QObject(parent)
0170     , m_messageBox(nullptr)
0171 {
0172     // job control action
0173     m_controlAction = new KToolBarPopupAction(Icon("media-playback-pause"), i18n("Play/Pause &Job"), this);
0174     m_controlAction->setEnabled(false);
0175     connect(m_controlAction, &QAction::triggered, this, &JobMan::slotControlActionTriggered);
0176 
0177     auto *menu = new QMenu(krMainWindow);
0178     menu->setMinimumWidth(300);
0179     // make scrollable if menu is too long
0180     menu->setStyleSheet("QMenu { menu-scrollable: 1; }");
0181     m_controlAction->setMenu(menu);
0182 
0183     // progress bar action
0184     m_progressBar = new QProgressBar();
0185     m_progressBar->setToolTip(sDefaultToolTip);
0186     m_progressBar->setEnabled(false);
0187     // listen to clicks on progress bar
0188     m_progressBar->installEventFilter(this);
0189 
0190     auto *progressAction = new QWidgetAction(krMainWindow);
0191     progressAction->setText(i18n("Job Progress Bar"));
0192     progressAction->setDefaultWidget(m_progressBar);
0193     m_progressAction = progressAction;
0194 
0195     // job queue mode action
0196     KConfigGroup cfg(krConfig, "JobManager");
0197     m_queueMode = cfg.readEntry("Queue Mode", false);
0198     m_modeAction = new QAction(Icon("media-playlist-repeat"), i18n("Job Queue Mode"), krMainWindow);
0199     m_modeAction->setToolTip(i18n("Run only one job in parallel"));
0200     m_modeAction->setCheckable(true);
0201     m_modeAction->setChecked(m_queueMode);
0202     connect(m_modeAction, &QAction::toggled, this, [=](bool checked) mutable {
0203         m_queueMode = checked;
0204         cfg.writeEntry("Queue Mode", m_queueMode);
0205     });
0206 
0207     // undo action
0208     KIO::FileUndoManager *undoManager = KIO::FileUndoManager::self();
0209     undoManager->uiInterface()->setParentWidget(krMainWindow);
0210 
0211     m_undoAction = new QAction(Icon("edit-undo"), i18n("Undo Last Job"), krMainWindow);
0212     m_undoAction->setEnabled(false);
0213     connect(m_undoAction, &QAction::triggered, undoManager, &KIO::FileUndoManager::undo);
0214     connect(undoManager, static_cast<void (KIO::FileUndoManager::*)(bool)>(&KIO::FileUndoManager::undoAvailable), m_undoAction, &QAction::setEnabled);
0215     connect(undoManager, &KIO::FileUndoManager::undoTextChanged, this, &JobMan::slotUndoTextChange);
0216 }
0217 
0218 bool JobMan::waitForJobs(bool waitForUserInput)
0219 {
0220     if (m_jobs.isEmpty() && !waitForUserInput)
0221         return true;
0222 
0223     // attempt to get all job threads does not work
0224     // QList<QThread *> threads = krMainWindow->findChildren<QThread *>();
0225 
0226     m_autoCloseMessageBox = !waitForUserInput;
0227 
0228     m_messageBox = new QMessageBox(krMainWindow);
0229     m_messageBox->setWindowTitle(i18n("Warning"));
0230     m_messageBox->setIconPixmap(Icon("dialog-warning").pixmap(QMessageBox::standardIcon(QMessageBox::Information).size()));
0231     m_messageBox->setText(i18n("Are you sure you want to quit?"));
0232     m_messageBox->addButton(QMessageBox::Abort);
0233     m_messageBox->addButton(QMessageBox::Cancel);
0234     m_messageBox->setDefaultButton(QMessageBox::Cancel);
0235     for (KrJob *job : qAsConst(m_jobs))
0236         connect(job, &KrJob::terminated, this, &JobMan::slotUpdateMessageBox);
0237     slotUpdateMessageBox();
0238 
0239     int result = m_messageBox->exec(); // blocking
0240     m_messageBox->deleteLater();
0241     m_messageBox = nullptr;
0242 
0243     // accepted -> cancel all jobs
0244     if (result == QMessageBox::Abort) {
0245         for (KrJob *job : qAsConst(m_jobs)) {
0246             job->cancel();
0247         }
0248         return true;
0249     }
0250     // else:
0251     return false;
0252 }
0253 
0254 void JobMan::manageJob(KrJob *job, StartMode startMode)
0255 {
0256     qDebug() << "new job, startMode=" << startMode;
0257     managePrivate(job);
0258 
0259     connect(job, &KrJob::started, this, &JobMan::slotKJobStarted);
0260 
0261     const bool enqueue = startMode == Enqueue || (startMode == Default && m_queueMode);
0262     if (startMode == Start || (startMode == Default && !m_queueMode) || (enqueue && !jobsAreRunning())) {
0263         job->start();
0264     }
0265 
0266     updateUI();
0267 }
0268 
0269 void JobMan::manageStartedJob(KrJob *krJob, KJob *kJob)
0270 {
0271     managePrivate(krJob, kJob);
0272     slotKJobStarted(kJob);
0273     updateUI();
0274 }
0275 
0276 // #### protected slots
0277 
0278 void JobMan::slotKJobStarted(KJob *job)
0279 {
0280     // KJob has two percent() functions
0281     connect(job, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotPercent(KJob *, ulong)));
0282     connect(job, &KJob::description, this, &JobMan::slotDescription);
0283     connect(job, &KJob::suspended, this, &JobMan::updateUI);
0284     connect(job, &KJob::resumed, this, &JobMan::updateUI);
0285 }
0286 
0287 void JobMan::slotControlActionTriggered()
0288 {
0289     if (m_jobs.isEmpty()) {
0290         m_controlAction->menu()->clear();
0291         m_controlAction->setEnabled(false);
0292         return;
0293     }
0294 
0295     const bool anyRunning = jobsAreRunning();
0296     if (!anyRunning && m_queueMode) {
0297         m_jobs.first()->start();
0298     } else {
0299         for (KrJob *job : qAsConst(m_jobs)) {
0300             if (anyRunning)
0301                 job->pause();
0302             else
0303                 job->start();
0304         }
0305     }
0306 }
0307 
0308 void JobMan::slotPercent(KJob *, unsigned long)
0309 {
0310     updateUI();
0311 }
0312 
0313 void JobMan::slotDescription(KJob *, const QString &description, const QPair<QString, QString> &field1, const QPair<QString, QString> &field2)
0314 {
0315     // TODO cache all descriptions
0316     if (m_jobs.length() > 1)
0317         return;
0318 
0319     m_progressBar->setToolTip(QString("%1\n%2: %3\n%4: %5").arg(description, field1.first, field1.second, field2.first, field2.second));
0320 }
0321 
0322 void JobMan::slotTerminated(KrJob *krJob)
0323 {
0324     qDebug() << "terminated, job description: " << krJob->description();
0325 
0326     m_jobs.removeAll(krJob);
0327 
0328     // NOTE: ignoring queue mode here. We assume that if queue mode is turned off, the user created
0329     // jobs which were not already started with a "queue" option and still wants queue behaviour.
0330     if (!m_jobs.isEmpty() && !jobsAreRunning()) {
0331         foreach (KrJob *job, m_jobs) {
0332             if (!job->isPaused()) {
0333                 // start next job
0334                 job->start();
0335                 break;
0336             }
0337         }
0338     }
0339 
0340     updateUI();
0341     cleanupMenu();
0342 }
0343 
0344 void JobMan::slotUpdateControlAction()
0345 {
0346     m_controlAction->setEnabled(!m_controlAction->menu()->isEmpty());
0347 }
0348 
0349 void JobMan::slotUndoTextChange(const QString &text)
0350 {
0351 #if KIO_VERSION >= QT_VERSION_CHECK(5, 79, 0)
0352     bool isUndoAvailable = KIO::FileUndoManager::self()->isUndoAvailable();
0353 #else
0354     bool isUndoAvailable = KIO::FileUndoManager::self()->undoAvailable();
0355 #endif
0356 
0357     m_undoAction->setToolTip(isUndoAvailable ? text : i18n("Undo Last Job"));
0358 }
0359 
0360 void JobMan::slotUpdateMessageBox()
0361 {
0362     if (!m_messageBox)
0363         return;
0364 
0365     if (m_jobs.isEmpty() && m_autoCloseMessageBox) {
0366         m_messageBox->done(QMessageBox::Abort);
0367         return;
0368     }
0369 
0370     if (m_jobs.isEmpty()) {
0371         m_messageBox->setInformativeText("");
0372         m_messageBox->setButtonText(QMessageBox::Abort, "Quit");
0373         return;
0374     }
0375 
0376     m_messageBox->setInformativeText(i18np("There is one job operation left.", "There are %1 job operations left.", m_jobs.length()));
0377     m_messageBox->setButtonText(QMessageBox::Abort, "Abort Jobs and Quit");
0378 }
0379 
0380 // #### private
0381 
0382 void JobMan::managePrivate(KrJob *job, KJob *kJob)
0383 {
0384     auto *menuAction = new JobMenuAction(job, m_controlAction, kJob);
0385     connect(menuAction, &QObject::destroyed, this, &JobMan::slotUpdateControlAction);
0386     m_controlAction->menu()->addAction(menuAction);
0387     cleanupMenu();
0388 
0389     slotUpdateControlAction();
0390 
0391     connect(job, &KrJob::terminated, this, &JobMan::slotTerminated);
0392 
0393     m_jobs.append(job);
0394 }
0395 
0396 void JobMan::cleanupMenu()
0397 {
0398     const QList<QAction *> actions = m_controlAction->menu()->actions();
0399     for (QAction *action : actions) {
0400         if (m_controlAction->menu()->actions().count() <= MAX_OLD_MENU_ACTIONS)
0401             break;
0402         auto *jobAction = dynamic_cast<JobMenuAction *>(action);
0403         if (jobAction->isDone()) {
0404             m_controlAction->menu()->removeAction(action);
0405             action->deleteLater();
0406         }
0407     }
0408 }
0409 
0410 void JobMan::updateUI()
0411 {
0412     int totalPercent = 0;
0413     for (KrJob *job : qAsConst(m_jobs)) {
0414         totalPercent += job->percent();
0415     }
0416     const bool hasJobs = !m_jobs.isEmpty();
0417     m_progressBar->setEnabled(hasJobs);
0418     if (hasJobs) {
0419         m_progressBar->setValue(totalPercent / m_jobs.length());
0420     } else {
0421         m_progressBar->reset();
0422     }
0423     if (!hasJobs)
0424         m_progressBar->setToolTip(i18n("No Jobs"));
0425     if (m_jobs.length() > 1)
0426         m_progressBar->setToolTip(i18np("%1 Job", "%1 Jobs", m_jobs.length()));
0427 
0428     const bool running = jobsAreRunning();
0429     m_controlAction->setIcon(Icon(!hasJobs ? "edit-clear" : running ? "media-playback-pause" : "media-playback-start"));
0430     m_controlAction->setToolTip(!hasJobs ? i18n("Clear Job List") : running ? i18n("Pause All Jobs") : i18n("Resume Job List"));
0431 }
0432 
0433 bool JobMan::jobsAreRunning()
0434 {
0435     return std::any_of(m_jobs.cbegin(), m_jobs.cend(), [](KrJob *job) {
0436         return job->isRunning();
0437     });
0438 }