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 }